@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,182 @@
1
+ ---
2
+ title: "Quickstart"
3
+ description: "Publish bytes to a real objects directory, read them back, watch the address deduplicate a second put, and watch a corrupted blob get refused and then healed."
4
+ sidebar:
5
+ order: 2
6
+ ---
7
+
8
+ This quickstart runs the filesystem store against a real directory. By the end
9
+ you will have published an artifact, read it back, seen the address absorb a
10
+ second identical put, and seen the store refuse and then repair a blob you
11
+ corrupted on purpose.
12
+
13
+ ## Prerequisites
14
+
15
+ - Node.js 26.4.0 or later.
16
+ - An empty directory to work in. The store writes under `.flows/objects`
17
+ relative to the process working directory.
18
+ - The package and the Node host layers. Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
19
+
20
+ ## Compose the store
21
+
22
+ Create `quickstart.ts`. The filesystem store needs Effect's `FileSystem` to
23
+ reach the disk and `Crypto` to measure bytes, so the composition provides both
24
+ and keeps them visible for the rest of the walkthrough:
25
+
26
+ ```ts
27
+ import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
28
+ import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
29
+ import * as NodePath from "@effect/platform-node/NodePath"
30
+ import * as ArtifactStore from "@smthrs/artifacts/ArtifactStore"
31
+ import * as Effect from "effect/Effect"
32
+ import * as FileSystem from "effect/FileSystem"
33
+ import * as Layer from "effect/Layer"
34
+
35
+ const directory = ".flows/objects"
36
+ const payload = new TextEncoder().encode("the bytes a step produced")
37
+
38
+ const host = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer, NodeCrypto.layer)
39
+ const layer = ArtifactStore.layerFileSystem({ directory }).pipe(Layer.provideMerge(host))
40
+ ```
41
+
42
+ `Layer.provideMerge` feeds the host services into the store and keeps them in
43
+ the resulting layer, so the snippets below can use `FileSystem` directly to
44
+ tamper with a blob.
45
+
46
+ ## Publish an artifact and read it back
47
+
48
+ `put` measures the bytes, publishes them, and returns their address. `get`
49
+ verifies that the stored bytes still hash to the address before returning
50
+ them:
51
+
52
+ ```ts
53
+ const digest = await Effect.runPromise(
54
+ Effect.gen(function*() {
55
+ const store = yield* ArtifactStore.ArtifactStore
56
+ const address = yield* store.put(payload)
57
+
58
+ console.log("address:", address)
59
+ console.log("read back:", new TextDecoder().decode(yield* store.get(address)))
60
+ return address
61
+ }).pipe(Effect.provide(layer))
62
+ )
63
+ ```
64
+
65
+ Each snippet below is another statement in the same file. They all provide the
66
+ same `layer`, so they all reach the same directory.
67
+
68
+ ## Put the same bytes twice
69
+
70
+ There is no second copy and no second address. The store verifies the blob
71
+ already at the address, freshens its modification time so a retention sweep
72
+ reads it as recently referenced, and returns the same digest:
73
+
74
+ ```ts
75
+ const again = await Effect.runPromise(
76
+ Effect.gen(function*() {
77
+ const store = yield* ArtifactStore.ArtifactStore
78
+ return yield* store.put(payload)
79
+ }).pipe(Effect.provide(layer))
80
+ )
81
+
82
+ console.log("same address:", again === digest)
83
+ ```
84
+
85
+ ## Read an address nothing was published under
86
+
87
+ A well-formed address with no bytes behind it is `ArtifactMissing`, a distinct
88
+ tag rather than a generic failure, because a composition with a second tier
89
+ acts on it:
90
+
91
+ ```ts
92
+ const miss = await Effect.runPromise(
93
+ Effect.gen(function*() {
94
+ const store = yield* ArtifactStore.ArtifactStore
95
+ return yield* store.get("0".repeat(64))
96
+ }).pipe(
97
+ Effect.catchTag("@smthrs/artifacts/ArtifactMissing", (failure) => Effect.succeed(failure.code)),
98
+ Effect.provide(layer)
99
+ )
100
+ )
101
+
102
+ console.log("absent address:", miss)
103
+ ```
104
+
105
+ An address that is not 64 lowercase hexadecimal characters never reaches the
106
+ disk at all: it fails with an `ArtifactStoreError` whose code is
107
+ `invalid_digest`.
108
+
109
+ ## Corrupt a blob, then heal it
110
+
111
+ Overwrite the published blob with different bytes. The next read measures what
112
+ it found, sees that it no longer matches the address, and refuses. The repair
113
+ is an ordinary put: because the store verifies the blob already at an address
114
+ rather than trusting that the path exists, the mismatch falls through to the
115
+ atomic rewrite:
116
+
117
+ ```ts
118
+ await Effect.runPromise(
119
+ Effect.gen(function*() {
120
+ const store = yield* ArtifactStore.ArtifactStore
121
+ const fs = yield* FileSystem.FileSystem
122
+ const blob = `${directory}/${digest.slice(0, 2)}/${digest}`
123
+
124
+ yield* fs.writeFile(blob, new TextEncoder().encode("tampered"))
125
+
126
+ const refusal = yield* store.get(digest).pipe(
127
+ Effect.catchTag("@smthrs/artifacts/ArtifactCorruption", (failure) => Effect.succeed(failure.measuredDigest))
128
+ )
129
+ console.log("measured instead:", refusal)
130
+
131
+ yield* store.put(payload)
132
+ console.log("healed:", new TextDecoder().decode(yield* store.get(digest)))
133
+ }).pipe(Effect.provide(layer))
134
+ )
135
+ ```
136
+
137
+ ## Run it
138
+
139
+ Run the file with your TypeScript runner. The output is:
140
+
141
+ ```text
142
+ address: 077303668cf56af8d162bb5ccccd7127f2e8baff448bf5b649530a98e9c943da
143
+ read back: the bytes a step produced
144
+ same address: true
145
+ absent address: artifact_missing
146
+ measured instead: d121be3103007b41edf96f8262925f8c7d61894afe9a041843b631f69445bc57
147
+ healed: the bytes a step produced
148
+ ```
149
+
150
+ Look at what landed on disk:
151
+
152
+ ```bash
153
+ find .flows/objects -type f
154
+ ```
155
+
156
+ ```text
157
+ .flows/objects/07/077303668cf56af8d162bb5ccccd7127f2e8baff448bf5b649530a98e9c943da
158
+ ```
159
+
160
+ The two-hex directory is the digest's own prefix. It exists so a workspace that
161
+ publishes hundreds of thousands of artifacts never puts them all in one
162
+ directory.
163
+
164
+ ## What just happened
165
+
166
+ Every operation was addressed by content, so nothing above needed a file name,
167
+ a bucket, or an identifier of its own. The second put stored no second copy
168
+ because the address already existed and verified, but it was not free: it
169
+ hashed its input, rehashed the stored blob, took the digest's lock, freshened
170
+ the blob, and synced it. The corrupted read failed loudly instead of returning
171
+ bytes that were not the recorded artifact, and the repair was the same call
172
+ that published it the first time.
173
+
174
+ ## Next steps
175
+
176
+ - [Content addressing](./concepts/content-addressing.md): why each of those
177
+ outcomes is the only one the store can give.
178
+ - [Share artifacts across machines](./guides/share-artifacts-across-machines.md):
179
+ put the same store behind an HTTP cache so a second machine reads what this
180
+ one produced.
181
+ - [Test against an artifact store](./guides/test-against-an-artifact-store.md):
182
+ the in-memory store, which needs no directory and no temporary files.
@@ -0,0 +1,232 @@
1
+ ---
2
+ title: "Troubleshooting"
3
+ description: "Every failure @smthrs/artifacts reports, plus the silent problems that produce no failure at all: what happened, why, and what to change."
4
+ ---
5
+
6
+ Every failure here is typed. Read the tag first, then the `code` inside
7
+ `ArtifactStoreError`, then the message. The full schemas are on the
8
+ [API reference](./api.md).
9
+
10
+ Three tags exist, and they mean different things on purpose:
11
+
12
+ | Tag | Meaning |
13
+ | -------------------------------------- | ----------------------------------------------------------------- |
14
+ | `@smthrs/artifacts/ArtifactMissing` | An ordinary miss. Another tier may still satisfy it. |
15
+ | `@smthrs/artifacts/ArtifactCorruption` | An integrity violation. Bytes at an address no longer hash to it. |
16
+ | `@smthrs/artifacts/ArtifactStoreError` | The store, host, or transport refused. Carries a `code`. |
17
+
18
+ ## ArtifactMissing
19
+
20
+ **What happened.** The address is well formed, and this tier holds no bytes
21
+ under it. The error carries the validated `digest`.
22
+
23
+ **What to change.** Usually nothing: a read-through composition acts on this
24
+ rather than failing. If a blob you expect really is gone, check in this order:
25
+
26
+ - The objects directory. A store pointed at a different `directory` than the
27
+ one that published is the most common cause, and it looks exactly like a
28
+ cold cache.
29
+ - A sweep. `ArtifactSweep.remove` reclaimed it, which means the mark phase did
30
+ not consider it live.
31
+ - The layout. Addresses published under the pre-1.0 flat
32
+ `<directory>/<digest>` layout are misses that re-publish. There is no
33
+ compatibility shim.
34
+ - The shared tier. A combined `put` uploads opportunistically, so a blob local
35
+ to the producing machine may never have reached the cache. See
36
+ [The three tiers](./concepts/tiers.md).
37
+
38
+ ## ArtifactCorruption
39
+
40
+ **What happened.** The bytes found at an address do not hash to it. The error
41
+ carries both `recordedDigest` and `measuredDigest`.
42
+
43
+ **What to change.** Publish the correct bytes again. Because every put verifies
44
+ the existing blob rather than trusting the path, the mismatch falls through to
45
+ an atomic rewrite that heals the address. A combined store does this for you:
46
+ it fetches from the shared tier and writes back under every download policy,
47
+ including `minimal`.
48
+
49
+ If it recurs on one host, suspect the disk or another process writing into the
50
+ objects directory. If it recurs from one shared tier, suspect the tier: a
51
+ mis-serving cache is exactly what this check exists to catch.
52
+
53
+ ## invalid_digest
54
+
55
+ **Message.** `artifact digest must be exactly 64 lowercase hexadecimal characters`
56
+
57
+ **What happened.** A value that is not a canonical SHA-256 address reached a
58
+ store or sweep operation. The message is a constant, so it never echoes the
59
+ value.
60
+
61
+ **What to change.** Look at what produced the string. Uppercase hexadecimal, a
62
+ truncated digest, a filesystem path, and a `key1_`-prefixed step key are all
63
+ common. A step key is not an artifact address: keys come from
64
+ [`@smthrs/keys`](/api/keys) and address cache entries, while artifact addresses
65
+ come from `put`.
66
+
67
+ ## invalid_configuration
68
+
69
+ **What happened.** An option was refused at construction, before any request
70
+ left the process. Every message names the violated rule and nothing else.
71
+
72
+ | Message | Cause |
73
+ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
74
+ | `invalid remote artifact option: endpoint` | The endpoint is not a string. |
75
+ | `invalid remote artifact endpoint` | No `URL` parser accepts it. |
76
+ | `remote artifact endpoint must use HTTPS` | The scheme is not `https:`, and the host is not literal loopback (`localhost`, `127.0.0.1`, `[::1]`). |
77
+ | `remote artifact endpoint must not contain credentials, a query, or a fragment` | The endpoint carries userinfo, `?`, or `#`. |
78
+ | `invalid remote artifact option: options` | A header name is not an HTTP token, or a header value is not a string, exceeds 16 KiB, or has a control character. |
79
+ | `invalid remote artifact option: <name>` | A deadline, `maxDownloadBytes`, `maxFindMissingResponseBytes`, `chunkBytes`, or `downloadPolicy` is out of range. |
80
+ | `invalid combined artifact option: uploadTimeout` | The combined upload deadline is not a positive finite duration. |
81
+ | `invalid combined artifact option: downloadPolicy` | The policy is not `all`, `toplevel`, or `minimal`. |
82
+
83
+ **What to change.** Move a credential out of the URL and into
84
+ `RemoteArtifacts.Options.headers`, which is the credential seam. Note that
85
+ `maxFindMissingResponseBytes` may only lower the protocol's 256 KiB bound, so a
86
+ larger value is refused.
87
+
88
+ ## unavailable
89
+
90
+ **What happened.** A host refused, or the store has no implementation.
91
+
92
+ | Message | Cause |
93
+ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
94
+ | `the host filesystem refused an artifact operation: ...` | The `FileSystem` service refused a read, write, rename, sync, or lock. |
95
+ | `the host filesystem refused a sweep operation: ...` | The same, from `ArtifactSweep`. |
96
+ | `the host could not copy the artifact bytes` | The snapshot of the caller's buffer failed: a detached buffer, or an allocation the runtime would not give. |
97
+ | `<method> is unavailable` | A `makeNoop` store or sweep, with no override for that method. |
98
+
99
+ **What to change.** The cause carries the underlying host error. Two cases have
100
+ non-obvious fixes:
101
+
102
+ - A `TimeoutError` in the cause of a filesystem operation means lock
103
+ acquisition gave up after two minutes. Another process is holding the digest
104
+ lock, or the workspace-global backup-lease gate, and is still heartbeating.
105
+ Find it, or drop the store to `coordination: "process"` if cross-process
106
+ exclusion is not something this host needs.
107
+ - A sync refusal can use `durability: "best-effort"` when weaker durability is
108
+ acceptable. Both modes still require exclusive writable handles and symlink
109
+ inspection. If the host lacks either, use a memory or remote tier until the
110
+ host supplies them. A detected symlink or directory replacement also fails
111
+ as `unavailable`; audit the store before retrying.
112
+
113
+ `the shared upload was interrupted before it settled` is a special case: it is
114
+ the typed answer given to callers waiting on an in-flight combined upload that
115
+ was interrupted. A combined `put` already drops it, and the next put of that
116
+ digest starts a fresh upload.
117
+
118
+ ## digest_failed
119
+
120
+ **Message.** `the Crypto service failed to compute an artifact digest`
121
+
122
+ **What happened.** The injected `Crypto` service refused. Nothing about the
123
+ bytes or the store is implicated.
124
+
125
+ **What to change.** Check the `Crypto` layer the composition provides.
126
+ `NodeCrypto.layer` from `@effect/platform-node` is the Node implementation. A
127
+ composition with no `Crypto` at all does not compile, so this is a runtime
128
+ refusal, not a missing layer.
129
+
130
+ ## transport_failed
131
+
132
+ **What happened.** The shared tier answered in a way the protocol cannot use.
133
+
134
+ | Message | Cause |
135
+ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
136
+ | `the remote artifact tier refused <operation>` | The HTTP client could not complete the exchange, or the body could not be read. |
137
+ | `the remote artifact tier answered <operation> with HTTP <status>` | Any non-2xx other than the `404` that means a miss. |
138
+ | `the remote artifact tier answered <operation> with <n> bytes, past the <m>-byte bound` | A body past `maxDownloadBytes` or `maxFindMissingResponseBytes`, refused from `Content-Length` or mid-stream. |
139
+ | `the remote artifact tier did not finish <operation> within its configured deadline` | The exchange outlived `downloadTimeout`, `uploadTimeout`, or `requestTimeout`. |
140
+
141
+ **What to change.** Match the operation to the request in
142
+ [Serve the artifact protocol](./guides/serve-the-artifact-protocol.md). A
143
+ `403` on a `PUT` while `GET` works means a read credential where a write one is
144
+ needed. A `413` means the tier's request-body cap: the Smithers cache
145
+ services cap one body at 16 MiB, and no `chunkBytes` setting works around that,
146
+ because they refuse ranged `PUT` with `400`.
147
+
148
+ Raise a deadline only after checking that the tier is actually slow rather than
149
+ unreachable. `CombinedArtifacts.get` propagates a remote `ArtifactStoreError`,
150
+ including download timeouts. `CombinedArtifacts.put` ignores failed opportunistic
151
+ uploads. `ArtifactSync.hydrate` separately converts remote failure into replay
152
+ rejection.
153
+
154
+ ## An upload succeeded here but a second machine cannot read it
155
+
156
+ **What happened.** A combined `put` returns the local digest and uploads
157
+ opportunistically. If the shared tier refused or stalled, the upload was
158
+ dropped and the operation still succeeded, by design: a cache being unreachable
159
+ must not fail the step that produced the bytes.
160
+
161
+ **What to change.** Nothing, if you are relying on the publication protocol.
162
+ [`@smthrs/engine-store`](/api/engine-store)'s `ArtifactSync` runs `findMissing`,
163
+ uploads, and confirms before a cache entry becomes observable, so a dropped
164
+ upload costs one re-upload rather than correctness. If you are calling `put`
165
+ directly and need the shared copy, probe the shared tier with `has` or
166
+ `findMissing` afterwards.
167
+
168
+ ## The sweep enumerates nothing, or deletes nothing
169
+
170
+ **What happened.** `inventory` returned an empty array, or `remove` keeps
171
+ answering `false`.
172
+
173
+ **What to change.**
174
+
175
+ - **Empty inventory.** The sweep is pointed at a directory the store never
176
+ published into. A directory that does not exist is an empty inventory, not a
177
+ failure. Build the store and the sweep with the same `directory`.
178
+ - **`remove` answers `false`.** Three outcomes share that answer: the blob was
179
+ already gone, it failed the `ifUnmodifiedSinceMs` fence, or a live backup
180
+ lease fenced the deletion. The third is not progress, so do not count those
181
+ bytes as reclaimed; the next pass after the backup finishes removes them. See
182
+ [Reclaim disk space](./guides/reclaim-disk-space.md).
183
+ - **Foreign files survive.** Only paths in the canonical `xx/<digest>` fanout
184
+ shape are blobs. Temp files, lock files, nested paths, and anything else are
185
+ skipped rather than deleted, and so is a blob whose modification time the
186
+ host cannot report.
187
+
188
+ ## A sweep deleted a blob a writer was publishing
189
+
190
+ **What happened.** The store and the sweep disagree about `coordination`. A
191
+ sweep on `required` beside a store on `process` takes lock files no writer ever
192
+ observes: the fence reads as armed and protects nothing.
193
+
194
+ **What to change.** Build both with the same `coordination` and the same
195
+ `directory`. Nothing checks the pairing at runtime, and no error is raised.
196
+ See [Coordination between processes](./concepts/coordination.md).
197
+
198
+ ## The get counter looks too low
199
+
200
+ **What happened.** `flows_artifact_gets` and `flows_artifact_puts` count local
201
+ artifact store traffic only. `RemoteArtifacts` is uninstrumented, so a combined
202
+ read the shared tier served increments no get counter, and the write-back that
203
+ materializes it increments a put indistinguishable from a producer publishing
204
+ new bytes.
205
+
206
+ **What to change.** Read the counters as local traffic rather than as artifact
207
+ operations. There is no tier attribute; adding one would change the published
208
+ counter shape. The full accounting is in
209
+ [The three tiers](./concepts/tiers.md#what-the-metrics-count).
210
+
211
+ ## Chunked uploads are not chunking
212
+
213
+ **What happened.** `chunkBytes` is set, and every blob still travels as one
214
+ whole-blob `PUT`.
215
+
216
+ **What to change.** Probably nothing: that is the designed degradation. A tier
217
+ that answers the empty `Content-Range: bytes */{total}` probe with `400`,
218
+ `411`, or `416`, or that answers `2xx` to a chunk that does not complete the
219
+ blob, is treated as range-unaware, and the client sends the blob whole so it
220
+ always lands. Both Smithers cache services answer `400`.
221
+
222
+ If you control the tier and want resumable transfers, implement the `308`
223
+ sequence in
224
+ [Serve the artifact protocol](./guides/serve-the-artifact-protocol.md).
225
+
226
+ ## Related
227
+
228
+ - [API reference](./api.md): the exact error schemas and codes.
229
+ - [Content addressing](./concepts/content-addressing.md): why corruption is a
230
+ refusal rather than a returned value.
231
+ - [Coordination between processes](./concepts/coordination.md): the lock
232
+ bounds behind the timeout failures.
package/package.json CHANGED
@@ -1,6 +1,182 @@
1
1
  {
2
2
  "name": "@smthrs/artifacts",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "type": "module",
4
+ "version": "1.0.0-rc.3",
5
+ "license": "MIT",
6
+ "smthrs": {
7
+ "group": "engine"
8
+ },
9
+ "description": "Effect service for the Smithers content-addressed artifact store, local and remote",
10
+ "homepage": "https://github.com/smithersai/smithers",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/smithersai/smithers.git",
14
+ "directory": "packages/smithers/flows/artifacts"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/smithersai/smithers/issues"
18
+ },
19
+ "tags": [
20
+ "typescript",
21
+ "effect",
22
+ "cache",
23
+ "content-addressed"
24
+ ],
25
+ "keywords": [
26
+ "typescript",
27
+ "effect",
28
+ "cache",
29
+ "content-addressed"
30
+ ],
31
+ "engines": {
32
+ "node": ">=26.4.0"
33
+ },
34
+ "sideEffects": [],
35
+ "exports": {
36
+ "./package.json": "./package.json",
37
+ ".": {
38
+ "import": {
39
+ "types": "./dist/esm/index.d.ts",
40
+ "default": "./dist/esm/index.js"
41
+ },
42
+ "require": {
43
+ "types": "./dist/cjs/index.d.ts",
44
+ "default": "./dist/cjs/index.js"
45
+ }
46
+ },
47
+ "./internal/*": null,
48
+ "./*/index": null,
49
+ "./ArtifactBackupLease": {
50
+ "import": {
51
+ "types": "./dist/esm/ArtifactBackupLease.d.ts",
52
+ "default": "./dist/esm/ArtifactBackupLease.js"
53
+ },
54
+ "require": {
55
+ "types": "./dist/cjs/ArtifactBackupLease.d.ts",
56
+ "default": "./dist/cjs/ArtifactBackupLease.js"
57
+ }
58
+ },
59
+ "./ArtifactStore": {
60
+ "import": {
61
+ "types": "./dist/esm/ArtifactStore.d.ts",
62
+ "default": "./dist/esm/ArtifactStore.js"
63
+ },
64
+ "require": {
65
+ "types": "./dist/cjs/ArtifactStore.d.ts",
66
+ "default": "./dist/cjs/ArtifactStore.js"
67
+ }
68
+ },
69
+ "./ArtifactStoreMetrics": {
70
+ "import": {
71
+ "types": "./dist/esm/ArtifactStoreMetrics.d.ts",
72
+ "default": "./dist/esm/ArtifactStoreMetrics.js"
73
+ },
74
+ "require": {
75
+ "types": "./dist/cjs/ArtifactStoreMetrics.d.ts",
76
+ "default": "./dist/cjs/ArtifactStoreMetrics.js"
77
+ }
78
+ },
79
+ "./ArtifactSweep": {
80
+ "import": {
81
+ "types": "./dist/esm/ArtifactSweep.d.ts",
82
+ "default": "./dist/esm/ArtifactSweep.js"
83
+ },
84
+ "require": {
85
+ "types": "./dist/cjs/ArtifactSweep.d.ts",
86
+ "default": "./dist/cjs/ArtifactSweep.js"
87
+ }
88
+ },
89
+ "./CombinedArtifacts": {
90
+ "import": {
91
+ "types": "./dist/esm/CombinedArtifacts.d.ts",
92
+ "default": "./dist/esm/CombinedArtifacts.js"
93
+ },
94
+ "require": {
95
+ "types": "./dist/cjs/CombinedArtifacts.d.ts",
96
+ "default": "./dist/cjs/CombinedArtifacts.js"
97
+ }
98
+ },
99
+ "./FileLease": {
100
+ "import": {
101
+ "types": "./dist/esm/FileLease.d.ts",
102
+ "default": "./dist/esm/FileLease.js"
103
+ },
104
+ "require": {
105
+ "types": "./dist/cjs/FileLease.d.ts",
106
+ "default": "./dist/cjs/FileLease.js"
107
+ }
108
+ },
109
+ "./RemoteArtifacts": {
110
+ "import": {
111
+ "types": "./dist/esm/RemoteArtifacts.d.ts",
112
+ "default": "./dist/esm/RemoteArtifacts.js"
113
+ },
114
+ "require": {
115
+ "types": "./dist/cjs/RemoteArtifacts.d.ts",
116
+ "default": "./dist/cjs/RemoteArtifacts.js"
117
+ }
118
+ },
119
+ "./index": {
120
+ "import": {
121
+ "types": "./dist/esm/index.d.ts",
122
+ "default": "./dist/esm/index.js"
123
+ },
124
+ "require": {
125
+ "types": "./dist/cjs/index.d.ts",
126
+ "default": "./dist/cjs/index.js"
127
+ }
128
+ }
129
+ },
130
+ "files": [
131
+ "src/**/*.ts",
132
+ "dist/**/*.js",
133
+ "dist/**/*.js.map",
134
+ "dist/**/*.d.ts",
135
+ "dist/**/*.d.ts.map",
136
+ "dist/**/package.json",
137
+ "LICENSE",
138
+ "README.md",
139
+ "CHANGELOG.md",
140
+ "docs/"
141
+ ],
142
+ "publishConfig": {
143
+ "access": "public",
144
+ "provenance": true,
145
+ "tag": "next"
146
+ },
147
+ "dependencies": {
148
+ "@smthrs/crypto": "1.0.0-rc.3"
149
+ },
150
+ "devDependencies": {
151
+ "@effect/language-service": "0.87.2",
152
+ "@effect/platform-node": "4.0.0-rc.115",
153
+ "@effect/vitest": "4.0.0-rc.115",
154
+ "@eslint/js": "9.39.5",
155
+ "@types/node": "26.4.1",
156
+ "@vitest/coverage-v8": "5.0.0",
157
+ "dprint": "0.57.1",
158
+ "effect": "4.0.0-rc.115",
159
+ "esbuild": "0.28.2",
160
+ "eslint": "9.39.5",
161
+ "eslint-import-resolver-typescript": "4.4.5",
162
+ "eslint-plugin-import": "2.32.0",
163
+ "eslint-plugin-jsdoc": "64.3.4",
164
+ "eslint-plugin-unicorn": "65.0.0",
165
+ "madge": "8.0.0",
166
+ "typescript": "5.9.3",
167
+ "typescript-eslint": "8.69.0",
168
+ "vitest": "5.0.0"
169
+ },
170
+ "peerDependencies": {
171
+ "effect": "4.0.0-rc.115"
172
+ },
173
+ "scripts": {
174
+ "lint": "eslint src --max-warnings=0 && dprint check",
175
+ "format": "dprint fmt",
176
+ "build": "node scripts/build.mjs",
177
+ "check": "tsc -b tsconfig.json && tsc -p tsconfig.test.json --noEmit",
178
+ "circular": "node scripts/circular.mjs",
179
+ "test": "vitest",
180
+ "coverage": "vitest --coverage"
181
+ }
6
182
  }