@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.
- package/CHANGELOG.md +77 -0
- package/LICENSE +21 -0
- package/README.md +173 -2
- package/dist/cjs/ArtifactBackupLease.d.ts +34 -0
- package/dist/cjs/ArtifactBackupLease.d.ts.map +1 -0
- package/dist/cjs/ArtifactBackupLease.js +175 -0
- package/dist/cjs/ArtifactBackupLease.js.map +7 -0
- package/dist/cjs/ArtifactStore.d.ts +129 -0
- package/dist/cjs/ArtifactStore.d.ts.map +1 -0
- package/dist/cjs/ArtifactStore.js +73 -0
- package/dist/cjs/ArtifactStore.js.map +7 -0
- package/dist/cjs/ArtifactStoreError.d.ts +82 -0
- package/dist/cjs/ArtifactStoreError.d.ts.map +1 -0
- package/dist/cjs/ArtifactStoreError.js +72 -0
- package/dist/cjs/ArtifactStoreError.js.map +7 -0
- package/dist/cjs/ArtifactStoreMetrics.d.ts +70 -0
- package/dist/cjs/ArtifactStoreMetrics.d.ts.map +1 -0
- package/dist/cjs/ArtifactStoreMetrics.js +51 -0
- package/dist/cjs/ArtifactStoreMetrics.js.map +7 -0
- package/dist/cjs/ArtifactSweep.d.ts +185 -0
- package/dist/cjs/ArtifactSweep.d.ts.map +1 -0
- package/dist/cjs/ArtifactSweep.js +163 -0
- package/dist/cjs/ArtifactSweep.js.map +7 -0
- package/dist/cjs/CombinedArtifacts.d.ts +103 -0
- package/dist/cjs/CombinedArtifacts.d.ts.map +1 -0
- package/dist/cjs/CombinedArtifacts.js +189 -0
- package/dist/cjs/CombinedArtifacts.js.map +7 -0
- package/dist/cjs/FileLease.d.ts +113 -0
- package/dist/cjs/FileLease.d.ts.map +1 -0
- package/dist/cjs/FileLease.js +201 -0
- package/dist/cjs/FileLease.js.map +7 -0
- package/dist/cjs/FileSystemArtifactStore.d.ts +93 -0
- package/dist/cjs/FileSystemArtifactStore.d.ts.map +1 -0
- package/dist/cjs/FileSystemArtifactStore.js +276 -0
- package/dist/cjs/FileSystemArtifactStore.js.map +7 -0
- package/dist/cjs/MemoryArtifactStore.d.ts +25 -0
- package/dist/cjs/MemoryArtifactStore.d.ts.map +1 -0
- package/dist/cjs/MemoryArtifactStore.js +84 -0
- package/dist/cjs/MemoryArtifactStore.js.map +7 -0
- package/dist/cjs/NoopArtifactStore.d.ts +17 -0
- package/dist/cjs/NoopArtifactStore.d.ts.map +1 -0
- package/dist/cjs/NoopArtifactStore.js +46 -0
- package/dist/cjs/NoopArtifactStore.js.map +7 -0
- package/dist/cjs/RemoteArtifacts.d.ts +237 -0
- package/dist/cjs/RemoteArtifacts.d.ts.map +1 -0
- package/dist/cjs/RemoteArtifacts.js +405 -0
- package/dist/cjs/RemoteArtifacts.js.map +7 -0
- package/dist/cjs/index.d.ts +65 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +47 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/ArtifactLocks.d.ts +30 -0
- package/dist/cjs/internal/ArtifactLocks.d.ts.map +1 -0
- package/dist/cjs/internal/ArtifactLocks.js +93 -0
- package/dist/cjs/internal/ArtifactLocks.js.map +7 -0
- package/dist/cjs/internal/ArtifactPath.d.ts +19 -0
- package/dist/cjs/internal/ArtifactPath.d.ts.map +1 -0
- package/dist/cjs/internal/ArtifactPath.js +74 -0
- package/dist/cjs/internal/ArtifactPath.js.map +7 -0
- package/dist/cjs/measureBytes.d.ts +18 -0
- package/dist/cjs/measureBytes.d.ts.map +1 -0
- package/dist/cjs/measureBytes.js +43 -0
- package/dist/cjs/measureBytes.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/snapshotBytes.d.ts +19 -0
- package/dist/cjs/snapshotBytes.d.ts.map +1 -0
- package/dist/cjs/snapshotBytes.js +41 -0
- package/dist/cjs/snapshotBytes.js.map +7 -0
- package/dist/cjs/validateDigest.d.ts +22 -0
- package/dist/cjs/validateDigest.d.ts.map +1 -0
- package/dist/cjs/validateDigest.js +42 -0
- package/dist/cjs/validateDigest.js.map +7 -0
- package/dist/esm/ArtifactBackupLease.d.ts +34 -0
- package/dist/esm/ArtifactBackupLease.d.ts.map +1 -0
- package/dist/esm/ArtifactBackupLease.js +117 -0
- package/dist/esm/ArtifactBackupLease.js.map +1 -0
- package/dist/esm/ArtifactStore.d.ts +129 -0
- package/dist/esm/ArtifactStore.d.ts.map +1 -0
- package/dist/esm/ArtifactStore.js +83 -0
- package/dist/esm/ArtifactStore.js.map +1 -0
- package/dist/esm/ArtifactStoreError.d.ts +82 -0
- package/dist/esm/ArtifactStoreError.d.ts.map +1 -0
- package/dist/esm/ArtifactStoreError.js +77 -0
- package/dist/esm/ArtifactStoreError.js.map +1 -0
- package/dist/esm/ArtifactStoreMetrics.d.ts +70 -0
- package/dist/esm/ArtifactStoreMetrics.d.ts.map +1 -0
- package/dist/esm/ArtifactStoreMetrics.js +77 -0
- package/dist/esm/ArtifactStoreMetrics.js.map +1 -0
- package/dist/esm/ArtifactSweep.d.ts +185 -0
- package/dist/esm/ArtifactSweep.d.ts.map +1 -0
- package/dist/esm/ArtifactSweep.js +202 -0
- package/dist/esm/ArtifactSweep.js.map +1 -0
- package/dist/esm/CombinedArtifacts.d.ts +103 -0
- package/dist/esm/CombinedArtifacts.d.ts.map +1 -0
- package/dist/esm/CombinedArtifacts.js +214 -0
- package/dist/esm/CombinedArtifacts.js.map +1 -0
- package/dist/esm/FileLease.d.ts +113 -0
- package/dist/esm/FileLease.d.ts.map +1 -0
- package/dist/esm/FileLease.js +239 -0
- package/dist/esm/FileLease.js.map +1 -0
- package/dist/esm/FileSystemArtifactStore.d.ts +93 -0
- package/dist/esm/FileSystemArtifactStore.d.ts.map +1 -0
- package/dist/esm/FileSystemArtifactStore.js +336 -0
- package/dist/esm/FileSystemArtifactStore.js.map +1 -0
- package/dist/esm/MemoryArtifactStore.d.ts +25 -0
- package/dist/esm/MemoryArtifactStore.d.ts.map +1 -0
- package/dist/esm/MemoryArtifactStore.js +67 -0
- package/dist/esm/MemoryArtifactStore.js.map +1 -0
- package/dist/esm/NoopArtifactStore.d.ts +17 -0
- package/dist/esm/NoopArtifactStore.d.ts.map +1 -0
- package/dist/esm/NoopArtifactStore.js +27 -0
- package/dist/esm/NoopArtifactStore.js.map +1 -0
- package/dist/esm/RemoteArtifacts.d.ts +237 -0
- package/dist/esm/RemoteArtifacts.d.ts.map +1 -0
- package/dist/esm/RemoteArtifacts.js +554 -0
- package/dist/esm/RemoteArtifacts.js.map +1 -0
- package/dist/esm/index.d.ts +65 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +65 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/ArtifactLocks.d.ts +30 -0
- package/dist/esm/internal/ArtifactLocks.d.ts.map +1 -0
- package/dist/esm/internal/ArtifactLocks.js +86 -0
- package/dist/esm/internal/ArtifactLocks.js.map +1 -0
- package/dist/esm/internal/ArtifactPath.d.ts +19 -0
- package/dist/esm/internal/ArtifactPath.d.ts.map +1 -0
- package/dist/esm/internal/ArtifactPath.js +59 -0
- package/dist/esm/internal/ArtifactPath.js.map +1 -0
- package/dist/esm/measureBytes.d.ts +18 -0
- package/dist/esm/measureBytes.d.ts.map +1 -0
- package/dist/esm/measureBytes.js +22 -0
- package/dist/esm/measureBytes.js.map +1 -0
- package/dist/esm/snapshotBytes.d.ts +19 -0
- package/dist/esm/snapshotBytes.d.ts.map +1 -0
- package/dist/esm/snapshotBytes.js +30 -0
- package/dist/esm/snapshotBytes.js.map +1 -0
- package/dist/esm/validateDigest.d.ts +22 -0
- package/dist/esm/validateDigest.d.ts.map +1 -0
- package/dist/esm/validateDigest.js +26 -0
- package/dist/esm/validateDigest.js.map +1 -0
- package/docs/README.md +200 -0
- package/docs/api.md +474 -0
- package/docs/concepts/content-addressing.md +174 -0
- package/docs/concepts/coordination.md +141 -0
- package/docs/concepts/tiers.md +147 -0
- package/docs/guides/fence-a-backup.md +123 -0
- package/docs/guides/reclaim-disk-space.md +143 -0
- package/docs/guides/serve-the-artifact-protocol.md +140 -0
- package/docs/guides/share-artifacts-across-machines.md +180 -0
- package/docs/guides/test-against-an-artifact-store.md +158 -0
- package/docs/installation.md +98 -0
- package/docs/quickstart.md +182 -0
- package/docs/troubleshooting.md +232 -0
- package/package.json +179 -3
- package/src/ArtifactBackupLease.ts +213 -0
- package/src/ArtifactStore.ts +152 -0
- package/src/ArtifactStoreError.ts +96 -0
- package/src/ArtifactStoreMetrics.ts +83 -0
- package/src/ArtifactSweep.ts +336 -0
- package/src/CombinedArtifacts.ts +369 -0
- package/src/FileLease.ts +380 -0
- package/src/FileSystemArtifactStore.ts +459 -0
- package/src/MemoryArtifactStore.ts +83 -0
- package/src/NoopArtifactStore.ts +30 -0
- package/src/RemoteArtifacts.ts +809 -0
- package/src/index.ts +71 -0
- package/src/internal/ArtifactLocks.ts +110 -0
- package/src/internal/ArtifactPath.ts +72 -0
- package/src/measureBytes.ts +28 -0
- package/src/snapshotBytes.ts +34 -0
- 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
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0-rc.4",
|
|
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.4"
|
|
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
|
}
|