@btravstack/storage

中文文档
0.58.0发布于 2026/11/08MITTypeScriptESM + CJS零依赖构建来源已验证近 30 天下载 7563

A Storage port for @btravstack/core: an in-memory adapter, an S3-compatible adapter with presigned reads, and a composition that spans, counts and logs every operation

原始使用文档
文档目录4 节
  1. A worked example
  2. Options
  3. What it decides, and what it does not
  4. License

@btravstack/storage

The object-storage port for @btravstack/core: one Storage an application depends on, an in-memory adapter, an S3-compatible one with presigned reads and writes and streamed ones in bounded memory, and a span, a count and a log line per operation.

📖 Documentation · API Reference

pnpm add @btravstack/storage @btravstack/core @btravstack/config @btravstack/di unthrown

Four peer dependencies, plus @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner — optional, and needed only if you compose the S3 adapter. Instrumentation needs no peer of its own: the Logger, Tracer and Meter it depends on are the kernel's ports. Node >=22.

A worked example

Storing a document, and reading it back through a URL that carries no credentials:

import {
  Port as InvoicePort,
  Provider as InvoiceProvider,
} from "@btravstack/di";
import { Storage } from "@btravstack/storage";
import type { AsyncResult } from "unthrown";

class Invoices extends InvoicePort("ReadmeInvoices")<{
  readonly store: (
    tenantId: string,
    id: string,
    pdf: Uint8Array,
  ) => AsyncResult<void, never>;
  readonly link: (
    tenantId: string,
    id: string,
  ) => AsyncResult<string | undefined, never>;
}> {}

// The tenant is in the key because the port has no slot for one — the same
// rule a cache key follows.
const keyFor = (tenantId: string, id: string) =>
  `invoices/${tenantId}/${id}.pdf`;

export const invoices = InvoiceProvider(Invoices)({
  inject: { storage: Storage },
  sync: ({ storage }) => ({
    store: (tenantId, id, pdf) =>
      storage
        .put(keyFor(tenantId, id), pdf, { contentType: "application/pdf" })
        // A document that failed to store is YOUR decision to make; here it
        // is swallowed because an observer already recorded it and
        // counted it one layer down.
        .recoverErrCases((matcher) =>
          matcher.with(P.tag("StorageUnavailable"), P.tag("InvalidStorageOptions"), P.tag("ChecksumMismatch"), () => undefined),
        ),
    // Serving bytes through your own process is the anti-pattern this arm
    // exists to avoid: the caller fetches the object directly, for a minute.
    link: (tenantId, id) =>
      storage
        .presignedUrl(keyFor(tenantId, id), { ttlMs: 60_000 })
        .recoverErrCases((matcher) =>
          matcher
            .with(P.tag("StorageUnavailable"), () => undefined)
            .with(P.tag("PresignNotSupported"), () => undefined),
        ),
  }),
});

The composition — the adapter is the only decision, decided here and nowhere else:

import { storage } from "@btravstack/storage";
import { s3Storage } from "@btravstack/storage/s3";

export const InvoicesApp = Module("InvoicesApp")({
  imports: [
    // Instrumented by default, which is why `observability()` and `otel()`
    // Every call is reported through `Observers`. Drop the two modules
    // below and this composition is unchanged — it just reports to nobody.
    storage({ adapter: s3Storage() }),
    observability(),
    otel(),
  ],
  provides: [invoices],
  exports: [Invoices, Logger],
});

Options

OptionWhereWhat it is
sha256 / metadataput / putIfAbsent and streamsverify the write digest and atomically describe the object
checksum / metadatacopycompute SHA-256 server side and replace the description
kmsKeyIdwrites, copy and presignedUploadselect per-object SSE-KMS; upload clients send both signed encryption headers
adapterstorage({ adapter })the adapter module providing StorageBackend — required
STORAGE_S3_ENDPOINTenvironment, read by s3Storage()the store's endpoint — required
STORAGE_S3_BUCKETenvironmentthe bucket — required
STORAGE_S3_ACCESS_KEY_ID / STORAGE_S3_SECRET_ACCESS_KEYenvironmentthe credentials — required
STORAGE_S3_REGIONenvironmentdefault us-east-1
STORAGE_S3_MAX_SOCKETS / maxSocketsenvironment, or s3Storage({...})sockets every call but the streamed ones shares — default 50, the SDK's
STORAGE_S3_STREAM_MAX_SOCKETS / streamMaxSocketsenvironment, or s3Storage({...})the streamed calls' own pool, and their bound — default 50
STORAGE_S3_CONNECTION_TIMEOUT_MS / connectionTimeoutMsenvironment, or s3Storage({...})how long an attempt waits for a socket — default 0, none
STORAGE_S3_SOCKET_TIMEOUT_MS / socketTimeoutMsenvironment, or s3Storage({...})how long an attempt's socket may stay idle — default 0, none
STORAGE_S3_REQUEST_TIMEOUT_MS / requestTimeoutMsenvironment, or s3Storage({...})how long an attempt waits for the store to answer — default 0, none
STORAGE_S3_STREAM_CONNECTION_TIMEOUT_MS / streamConnectionTimeoutMsenvironment, or s3Storage({...})the stream pool's own connection timeout — default connectionTimeoutMs
STORAGE_S3_STREAM_SOCKET_TIMEOUT_MS / streamSocketTimeoutMsenvironment, or s3Storage({...})the stream pool's own socket timeout — default socketTimeoutMs
STORAGE_S3_STREAM_REQUEST_TIMEOUT_MS / streamRequestTimeoutMsenvironment, or s3Storage({...})the stream pool's own request timeout — default requestTimeoutMs

The full table — defaults, semantics and the reasoning — lives on the reference page, which is this list's one detailed home.

What it decides, and what it does not

It decides that a missing object is an ordinary answer rather than a fault (counted not_found, without a fault line), that a held key under putIfAbsent is an ordinary answer (ObjectExists, counted exists), that delete is idempotent, that copy and head never move the bytes through the process, that writes can verify SHA-256 and publish user metadata atomically, that copies replace metadata and can compute SHA-256 server side, that presigned uploads can require a per-object KMS key, that a streamed write never leaves a partial object (a multipart upload, aborted on any failure), that streamed transfers have a socket pool of their own and are refused (StreamPoolFull, counted saturated) rather than queued once it is full, that presigning asks the store nothing — and that an adapter which cannot presign says so instead of minting a URL that would fail only in production.

It does not decide what your keys look like, whether a failed write should fail your request, or how long a link should live. There is no listing and no bucket management — each reason is on the reference page, and the same reasoning is in AGENTS.md for contributors.

License

MIT

Presigned downloads accept contentDisposition, contentType and cacheControl response overrides. See the storage reference for signing and Unicode filename semantics.

版本与发布记录

@btravstack/storage 共发布 62 个版本,首次发布于 2026/08/26;过去 12 个月发布了 62 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.58.0latest2026/11/08今天
0.57.02026/11/071 天前
0.56.12026/11/062 天前
0.56.02026/11/062 天前
0.55.12026/11/035 天前
0.55.02026/11/035 天前

关于 @btravstack/storage 的常见问题

@btravstack/storage 是什么?

A Storage port for @btravstack/core: an in-memory adapter, an S3-compatible adapter with presigned reads, and a composition that spans, counts and logs every operation

如何安装 @btravstack/storage?

在项目目录运行 npm install @btravstack/storage,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:unthrown(^5.0.0)、@btravstack/di(^0.58.0)、@btravstack/core(^0.58.0)、@btravstack/config(^0.58.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@btravstack/storage 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@btravstack/storage 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@btravstack/storage 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@btravstack/storage 可以商用吗?

@btravstack/storage 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@btravstack/storage 的下载量有多少?

根据 npm 官方下载统计,@btravstack/storage 在 2026/10/09 – 2026/11/07 这 30 天内被下载 7,563 次,日均约 252 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@btravstack/storage 还在维护吗?

最新版本 0.58.0 发布于 2026/11/08(今天);至今共发布 62 个版本,其中过去 12 个月发布了 62 个;首次发布于 2026/08/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。