@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,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.
|