@anabranch/storage 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Frodi Karlsson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,6 @@
1
+ # @anabranch/storage
2
+
3
+ Object storage primitives with Task/Stream semantics for error-tolerant
4
+ operations.
5
+
6
+ See [index.ts](./index.ts) for full documentation and examples.
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Input body types for put operations.
3
+ */
4
+ export type BodyInput = Uint8Array | ReadableStream | string;
5
+ /**
6
+ * Object metadata returned by head operations and included with get results.
7
+ */
8
+ export interface StorageMetadata {
9
+ key: string;
10
+ size: number;
11
+ etag?: string;
12
+ lastModified: Date;
13
+ contentType?: string;
14
+ custom?: Record<string, string>;
15
+ }
16
+ /**
17
+ * Object returned by get operations, containing a body stream and metadata.
18
+ */
19
+ export interface StorageObject {
20
+ body: ReadableStream;
21
+ metadata: StorageMetadata;
22
+ }
23
+ /**
24
+ * Entry returned by list operations.
25
+ */
26
+ export interface StorageEntry {
27
+ key: string;
28
+ size: number;
29
+ lastModified: Date;
30
+ }
31
+ /** Options for put operations. */
32
+ export interface PutOptions {
33
+ contentType?: string;
34
+ custom?: Record<string, string>;
35
+ }
36
+ /** Options for presign operations. */
37
+ export interface PresignOptions {
38
+ expiresIn: number;
39
+ method?: "GET" | "PUT";
40
+ }
41
+ /**
42
+ * Low-level storage adapter interface.
43
+ * Implement this to create drivers for specific storage backends.
44
+ */
45
+ export interface StorageAdapter {
46
+ /** Put an object into storage. */
47
+ put(key: string, body: BodyInput, options?: PutOptions): Promise<void>;
48
+ /** Get an object from storage. */
49
+ get(key: string): Promise<StorageObject>;
50
+ /** Delete an object from storage. */
51
+ delete(key: string): Promise<void>;
52
+ /** Get metadata without fetching the body. */
53
+ head(key: string): Promise<StorageMetadata>;
54
+ /** List objects with optional prefix. */
55
+ list(prefix?: string): AsyncIterable<StorageEntry>;
56
+ /** Release the connection back to its source. */
57
+ close(): Promise<void>;
58
+ }
59
+ /**
60
+ * Extended adapter interface for backends that support presigned URLs.
61
+ */
62
+ export interface PresignableAdapter extends StorageAdapter {
63
+ /** Generate a presigned URL for direct access. */
64
+ presign(key: string, options?: PresignOptions): Promise<string>;
65
+ }
66
+ /** Connector that produces connected StorageAdapter instances. */
67
+ export interface StorageConnector {
68
+ /** Acquire a connected adapter. */
69
+ connect(signal?: AbortSignal): Promise<StorageAdapter>;
70
+ /** Close all connections and clean up resources. */
71
+ end(): Promise<void>;
72
+ }
73
+ /** Storage configuration options. */
74
+ export interface StorageOptions {
75
+ /** Prefix for all keys in this storage. */
76
+ prefix?: string;
77
+ }
78
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,cAAc,GAAG,MAAM,CAAC;AAE7D;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,IAAI,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,cAAc,CAAC;IACrB,QAAQ,EAAE,eAAe,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,IAAI,CAAC;CACpB;AAED,kCAAkC;AAClC,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,sCAAsC;AACtC,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,kCAAkC;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,kCAAkC;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACzC,qCAAqC;IACrC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,8CAA8C;IAC9C,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC5C,yCAAyC;IACzC,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,YAAY,CAAC,CAAC;IACnD,iDAAiD;IACjD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,cAAc;IACxD,kDAAkD;IAClD,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACjE;AAED,kEAAkE;AAClE,MAAM,WAAW,gBAAgB;IAC/B,mCAAmC;IACnC,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACvD,oDAAoD;IACpD,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAED,qCAAqC;AACrC,MAAM,WAAW,cAAc;IAC7B,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
package/esm/adapter.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { StorageConnector, StorageOptions } from "./adapter.js";
2
+ /**
3
+ * Creates an in-memory storage connector for testing.
4
+ * Data is stored in memory and lost when the process ends.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const connector = createMemory({ prefix: "files/" });
9
+ * const storage = await connector.connect();
10
+ * await storage.put("test.txt", "Hello");
11
+ * ```
12
+ */
13
+ export declare function createMemory(options?: StorageOptions): StorageConnector;
14
+ //# sourceMappingURL=connector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../src/connector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,gBAAgB,EAEhB,cAAc,EACf,MAAM,cAAc,CAAC;AAKtB;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,gBAAgB,CAuFvE"}
@@ -0,0 +1,114 @@
1
+ import { StorageObjectNotFound } from "./errors.js";
2
+ const encoder = new TextEncoder();
3
+ /**
4
+ * Creates an in-memory storage connector for testing.
5
+ * Data is stored in memory and lost when the process ends.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const connector = createMemory({ prefix: "files/" });
10
+ * const storage = await connector.connect();
11
+ * await storage.put("test.txt", "Hello");
12
+ * ```
13
+ */
14
+ export function createMemory(options) {
15
+ const prefix = options?.prefix ?? "";
16
+ const data = new Map();
17
+ const metadata = new Map();
18
+ return {
19
+ connect: () => {
20
+ return Promise.resolve({
21
+ put: async (key, body, opts) => {
22
+ const fullKey = prefix + key;
23
+ const bytes = typeof body === "string"
24
+ ? encoder.encode(body)
25
+ : body instanceof ReadableStream
26
+ ? await bytesFromStream(body)
27
+ : body;
28
+ data.set(fullKey, bytes);
29
+ const size = bytes.length;
30
+ const lastModified = new Date();
31
+ metadata.set(fullKey, {
32
+ key: fullKey,
33
+ size,
34
+ lastModified,
35
+ contentType: opts?.contentType,
36
+ custom: opts?.custom,
37
+ });
38
+ },
39
+ get: (key) => {
40
+ const fullKey = prefix + key;
41
+ const bytes = data.get(fullKey);
42
+ if (!bytes) {
43
+ return Promise.reject(new StorageObjectNotFound(key));
44
+ }
45
+ const meta = metadata.get(fullKey);
46
+ if (!meta) {
47
+ return Promise.reject(new StorageObjectNotFound(key));
48
+ }
49
+ return Promise.resolve({
50
+ body: new ReadableStream({
51
+ start(controller) {
52
+ controller.enqueue(bytes);
53
+ controller.close();
54
+ },
55
+ }),
56
+ metadata: { ...meta },
57
+ });
58
+ },
59
+ delete: (key) => {
60
+ const fullKey = prefix + key;
61
+ data.delete(fullKey);
62
+ metadata.delete(fullKey);
63
+ return Promise.resolve();
64
+ },
65
+ head: (key) => {
66
+ const fullKey = prefix + key;
67
+ const meta = metadata.get(fullKey);
68
+ if (!meta) {
69
+ return Promise.reject(new StorageObjectNotFound(key));
70
+ }
71
+ return Promise.resolve({ ...meta });
72
+ },
73
+ list: (p) => {
74
+ const searchPrefix = prefix + (p ?? "");
75
+ return (async function* () {
76
+ for (const [key, meta] of metadata) {
77
+ if (key.startsWith(searchPrefix)) {
78
+ yield {
79
+ key,
80
+ size: meta.size,
81
+ lastModified: meta.lastModified,
82
+ };
83
+ }
84
+ }
85
+ })();
86
+ },
87
+ close: () => Promise.resolve(),
88
+ });
89
+ },
90
+ end: () => {
91
+ data.clear();
92
+ metadata.clear();
93
+ return Promise.resolve();
94
+ },
95
+ };
96
+ }
97
+ async function bytesFromStream(stream) {
98
+ const reader = stream.getReader();
99
+ const chunks = [];
100
+ while (true) {
101
+ const { done, value } = await reader.read();
102
+ if (done)
103
+ break;
104
+ chunks.push(value);
105
+ }
106
+ const total = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
107
+ const result = new Uint8Array(total);
108
+ let offset = 0;
109
+ for (const chunk of chunks) {
110
+ result.set(chunk, offset);
111
+ offset += chunk.length;
112
+ }
113
+ return result;
114
+ }
@@ -0,0 +1,37 @@
1
+ export declare class StorageConnectionFailed extends Error {
2
+ name: string;
3
+ constructor(message: string, cause?: unknown);
4
+ }
5
+ export declare class StorageObjectNotFound extends Error {
6
+ name: string;
7
+ constructor(key: string);
8
+ }
9
+ export declare class StorageCloseFailed extends Error {
10
+ name: string;
11
+ constructor(message: string, cause?: unknown);
12
+ }
13
+ export declare class StoragePutFailed extends Error {
14
+ name: string;
15
+ constructor(key: string, message: string, cause?: unknown);
16
+ }
17
+ export declare class StorageGetFailed extends Error {
18
+ name: string;
19
+ constructor(key: string, message: string, cause?: unknown);
20
+ }
21
+ export declare class StorageDeleteFailed extends Error {
22
+ name: string;
23
+ constructor(key: string, message: string, cause?: unknown);
24
+ }
25
+ export declare class StorageHeadFailed extends Error {
26
+ name: string;
27
+ constructor(key: string, message: string, cause?: unknown);
28
+ }
29
+ export declare class StoragePresignFailed extends Error {
30
+ name: string;
31
+ constructor(key: string, message: string, cause?: unknown);
32
+ }
33
+ export declare class StorageListFailed extends Error {
34
+ name: string;
35
+ constructor(prefix: string | undefined, message: string, cause?: unknown);
36
+ }
37
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,uBAAwB,SAAQ,KAAK;IACvC,IAAI,SAA6B;gBAExC,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO;CAIlB;AAED,qBAAa,qBAAsB,SAAQ,KAAK;IACrC,IAAI,SAA2B;gBAC5B,GAAG,EAAE,MAAM;CAGxB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAClC,IAAI,SAAwB;gBAEnC,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO;CAIlB;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IAChC,IAAI,SAAsB;gBAEjC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO;CAIlB;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IAChC,IAAI,SAAsB;gBAEjC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO;CAIlB;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IACnC,IAAI,SAAyB;gBAEpC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO;CAIlB;AAED,qBAAa,iBAAkB,SAAQ,KAAK;IACjC,IAAI,SAAuB;gBAElC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO;CAIlB;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IACpC,IAAI,SAA0B;gBAErC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO;CAIlB;AAED,qBAAa,iBAAkB,SAAQ,KAAK;IACjC,IAAI,SAAuB;gBAElC,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO;CASlB"}
package/esm/errors.js ADDED
@@ -0,0 +1,99 @@
1
+ export class StorageConnectionFailed extends Error {
2
+ constructor(message, cause) {
3
+ super(`Storage connection failed: ${message}`, { cause });
4
+ Object.defineProperty(this, "name", {
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true,
8
+ value: "StorageConnectionFailed"
9
+ });
10
+ }
11
+ }
12
+ export class StorageObjectNotFound extends Error {
13
+ constructor(key) {
14
+ super(`Storage object not found: ${key}`);
15
+ Object.defineProperty(this, "name", {
16
+ enumerable: true,
17
+ configurable: true,
18
+ writable: true,
19
+ value: "StorageObjectNotFound"
20
+ });
21
+ }
22
+ }
23
+ export class StorageCloseFailed extends Error {
24
+ constructor(message, cause) {
25
+ super(`Storage close failed: ${message}`, { cause });
26
+ Object.defineProperty(this, "name", {
27
+ enumerable: true,
28
+ configurable: true,
29
+ writable: true,
30
+ value: "StorageCloseFailed"
31
+ });
32
+ }
33
+ }
34
+ export class StoragePutFailed extends Error {
35
+ constructor(key, message, cause) {
36
+ super(`Failed to put object ${key}: ${message}`, { cause });
37
+ Object.defineProperty(this, "name", {
38
+ enumerable: true,
39
+ configurable: true,
40
+ writable: true,
41
+ value: "StoragePutFailed"
42
+ });
43
+ }
44
+ }
45
+ export class StorageGetFailed extends Error {
46
+ constructor(key, message, cause) {
47
+ super(`Failed to get object ${key}: ${message}`, { cause });
48
+ Object.defineProperty(this, "name", {
49
+ enumerable: true,
50
+ configurable: true,
51
+ writable: true,
52
+ value: "StorageGetFailed"
53
+ });
54
+ }
55
+ }
56
+ export class StorageDeleteFailed extends Error {
57
+ constructor(key, message, cause) {
58
+ super(`Failed to delete object ${key}: ${message}`, { cause });
59
+ Object.defineProperty(this, "name", {
60
+ enumerable: true,
61
+ configurable: true,
62
+ writable: true,
63
+ value: "StorageDeleteFailed"
64
+ });
65
+ }
66
+ }
67
+ export class StorageHeadFailed extends Error {
68
+ constructor(key, message, cause) {
69
+ super(`Failed to head object ${key}: ${message}`, { cause });
70
+ Object.defineProperty(this, "name", {
71
+ enumerable: true,
72
+ configurable: true,
73
+ writable: true,
74
+ value: "StorageHeadFailed"
75
+ });
76
+ }
77
+ }
78
+ export class StoragePresignFailed extends Error {
79
+ constructor(key, message, cause) {
80
+ super(`Failed to presign object ${key}: ${message}`, { cause });
81
+ Object.defineProperty(this, "name", {
82
+ enumerable: true,
83
+ configurable: true,
84
+ writable: true,
85
+ value: "StoragePresignFailed"
86
+ });
87
+ }
88
+ }
89
+ export class StorageListFailed extends Error {
90
+ constructor(prefix, message, cause) {
91
+ super(`Failed to list objects${prefix ? ` with prefix "${prefix}"` : ""}: ${message}`, { cause });
92
+ Object.defineProperty(this, "name", {
93
+ enumerable: true,
94
+ configurable: true,
95
+ writable: true,
96
+ value: "StorageListFailed"
97
+ });
98
+ }
99
+ }
package/esm/index.d.ts ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @anabranch/storage
3
+ *
4
+ * Storage primitives with Task/Stream semantics for error-tolerant object operations.
5
+ * Integrates with anabranch's {@linkcode Task}, {@linkcode Stream}, {@linkcode Source},
6
+ * and {@linkcode Channel} types for composable error handling and concurrent processing.
7
+ *
8
+ * ## Adapters vs Connectors
9
+ *
10
+ * A **StorageConnector** produces connected **StorageAdapter** instances. Use connectors
11
+ * for production code to properly manage connection lifecycles:
12
+ *
13
+ * - **Connector**: Manages connection pool/lifecycle, produces adapters
14
+ * - **Adapter**: Low-level put/get/delete/list interface
15
+ * - **Storage**: High-level wrapper with Task/Stream methods
16
+ *
17
+ * ## Core Types
18
+ *
19
+ * - {@link StorageConnector} - Interface for connection factories
20
+ * - {@link StorageAdapter} - Low-level storage operations interface
21
+ * - {@link StorageObject} - Retrieved object with body stream and metadata
22
+ * - {@link StorageMetadata} - Object metadata (size, contentType, etag, etc.)
23
+ *
24
+ * ## Error Types
25
+ *
26
+ * All errors are typed for catchable handling:
27
+ * - {@link StorageConnectionFailed} - Connection establishment failed
28
+ * - {@link StorageObjectNotFound} - Object does not exist
29
+ * - {@link StoragePutFailed} - Put operation failed
30
+ * - {@link StorageGetFailed} - Get operation failed
31
+ * - {@link StorageDeleteFailed} - Delete operation failed
32
+ * - {@link StorageHeadFailed} - Head operation failed
33
+ *
34
+ * @example Basic put/get operations with Storage wrapper
35
+ * ```ts
36
+ * import { Storage, createMemory } from "@anabranch/storage";
37
+ *
38
+ * const connector = createMemory({ prefix: "files/" });
39
+ * const storage = await Storage.connect(connector).run();
40
+ *
41
+ * await storage.put("hello.txt", "Hello, World!").run();
42
+ * const object = await storage.get("hello.txt").run();
43
+ * console.log(await new Response(object.body).text());
44
+ * ```
45
+ *
46
+ * @example Stream listing with concurrent processing
47
+ * ```ts
48
+ * const connector = createMemory();
49
+ * const storage = await Storage.connect(connector).run();
50
+ *
51
+ * await storage.put("users/1.json", '{"name": "Alice"}');
52
+ * await storage.put("users/2.json", '{"name": "Bob"}');
53
+ *
54
+ * const { successes, errors } = await storage.list("users/")
55
+ * .withConcurrency(5)
56
+ * .map(async (entry) => await processEntry(entry))
57
+ * .tapErr((err) => console.error("Failed:", err))
58
+ * .partition();
59
+ * ```
60
+ *
61
+ * @example Head request for metadata
62
+ * ```ts
63
+ * import { Storage, createMemory } from "@anabranch/storage";
64
+ *
65
+ * const connector = createMemory();
66
+ * const storage = await Storage.connect(connector).run();
67
+ *
68
+ * await storage.put("image.png", imageBytes, { contentType: "image/png" }).run();
69
+ * const metadata = await storage.head("image.png").run();
70
+ * console.log(metadata.contentType, metadata.size);
71
+ * ```
72
+ *
73
+ * @example With retry and timeout
74
+ * ```ts
75
+ * await storage.put("important.txt", data)
76
+ * .retry({ attempts: 3, delay: (attempt) => 100 * Math.pow(2, attempt) })
77
+ * .timeout(30_000)
78
+ * .run();
79
+ * ```
80
+ *
81
+ * @module
82
+ */
83
+ export { Storage } from "./storage.js";
84
+ export type { BodyInput, PresignableAdapter, PresignOptions, PutOptions, StorageAdapter, StorageConnector, StorageEntry, StorageMetadata, StorageObject, StorageOptions, } from "./adapter.js";
85
+ export * from "./errors.js";
86
+ export { createMemory } from "./connector.js";
87
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiFG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,YAAY,EACV,SAAS,EACT,kBAAkB,EAClB,cAAc,EACd,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,aAAa,EACb,cAAc,GACf,MAAM,cAAc,CAAC;AACtB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC"}
package/esm/index.js ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @anabranch/storage
3
+ *
4
+ * Storage primitives with Task/Stream semantics for error-tolerant object operations.
5
+ * Integrates with anabranch's {@linkcode Task}, {@linkcode Stream}, {@linkcode Source},
6
+ * and {@linkcode Channel} types for composable error handling and concurrent processing.
7
+ *
8
+ * ## Adapters vs Connectors
9
+ *
10
+ * A **StorageConnector** produces connected **StorageAdapter** instances. Use connectors
11
+ * for production code to properly manage connection lifecycles:
12
+ *
13
+ * - **Connector**: Manages connection pool/lifecycle, produces adapters
14
+ * - **Adapter**: Low-level put/get/delete/list interface
15
+ * - **Storage**: High-level wrapper with Task/Stream methods
16
+ *
17
+ * ## Core Types
18
+ *
19
+ * - {@link StorageConnector} - Interface for connection factories
20
+ * - {@link StorageAdapter} - Low-level storage operations interface
21
+ * - {@link StorageObject} - Retrieved object with body stream and metadata
22
+ * - {@link StorageMetadata} - Object metadata (size, contentType, etag, etc.)
23
+ *
24
+ * ## Error Types
25
+ *
26
+ * All errors are typed for catchable handling:
27
+ * - {@link StorageConnectionFailed} - Connection establishment failed
28
+ * - {@link StorageObjectNotFound} - Object does not exist
29
+ * - {@link StoragePutFailed} - Put operation failed
30
+ * - {@link StorageGetFailed} - Get operation failed
31
+ * - {@link StorageDeleteFailed} - Delete operation failed
32
+ * - {@link StorageHeadFailed} - Head operation failed
33
+ *
34
+ * @example Basic put/get operations with Storage wrapper
35
+ * ```ts
36
+ * import { Storage, createMemory } from "@anabranch/storage";
37
+ *
38
+ * const connector = createMemory({ prefix: "files/" });
39
+ * const storage = await Storage.connect(connector).run();
40
+ *
41
+ * await storage.put("hello.txt", "Hello, World!").run();
42
+ * const object = await storage.get("hello.txt").run();
43
+ * console.log(await new Response(object.body).text());
44
+ * ```
45
+ *
46
+ * @example Stream listing with concurrent processing
47
+ * ```ts
48
+ * const connector = createMemory();
49
+ * const storage = await Storage.connect(connector).run();
50
+ *
51
+ * await storage.put("users/1.json", '{"name": "Alice"}');
52
+ * await storage.put("users/2.json", '{"name": "Bob"}');
53
+ *
54
+ * const { successes, errors } = await storage.list("users/")
55
+ * .withConcurrency(5)
56
+ * .map(async (entry) => await processEntry(entry))
57
+ * .tapErr((err) => console.error("Failed:", err))
58
+ * .partition();
59
+ * ```
60
+ *
61
+ * @example Head request for metadata
62
+ * ```ts
63
+ * import { Storage, createMemory } from "@anabranch/storage";
64
+ *
65
+ * const connector = createMemory();
66
+ * const storage = await Storage.connect(connector).run();
67
+ *
68
+ * await storage.put("image.png", imageBytes, { contentType: "image/png" }).run();
69
+ * const metadata = await storage.head("image.png").run();
70
+ * console.log(metadata.contentType, metadata.size);
71
+ * ```
72
+ *
73
+ * @example With retry and timeout
74
+ * ```ts
75
+ * await storage.put("important.txt", data)
76
+ * .retry({ attempts: 3, delay: (attempt) => 100 * Math.pow(2, attempt) })
77
+ * .timeout(30_000)
78
+ * .run();
79
+ * ```
80
+ *
81
+ * @module
82
+ */
83
+ export { Storage } from "./storage.js";
84
+ export * from "./errors.js";
85
+ export { createMemory } from "./connector.js";
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,108 @@
1
+ import { Source, Task } from "anabranch";
2
+ import type { BodyInput, PutOptions, StorageAdapter, StorageConnector, StorageEntry, StorageMetadata, StorageObject } from "./adapter.js";
3
+ import { StorageCloseFailed, StorageConnectionFailed, StorageDeleteFailed, StorageGetFailed, StorageHeadFailed, StorageListFailed, StorageObjectNotFound, StoragePutFailed } from "./errors.js";
4
+ /**
5
+ * Storage wrapper with Task/Stream semantics for error-tolerant object operations.
6
+ *
7
+ * @example Basic put/get operations
8
+ * ```ts
9
+ * import { Storage, createMemory } from "@anabranch/storage";
10
+ *
11
+ * const connector = createMemory();
12
+ * const storage = await Storage.connect(connector).run();
13
+ *
14
+ * await storage.put("hello.txt", "Hello, World!").run();
15
+ * const object = await storage.get("hello.txt").run();
16
+ * ```
17
+ *
18
+ * @example Stream listing objects
19
+ * ```ts
20
+ * const { successes, errors } = await storage.list("users/")
21
+ * .withConcurrency(5)
22
+ * .map(async (entry) => await processEntry(entry))
23
+ * .tapErr((err) => console.error("Failed:", err))
24
+ * .partition();
25
+ * ```
26
+ *
27
+ * @example With retry and timeout
28
+ * ```ts
29
+ * await storage.put("important.txt", data)
30
+ * .retry({ attempts: 3, delay: (attempt) => 100 * Math.pow(2, attempt) })
31
+ * .timeout(30_000)
32
+ * .run();
33
+ * ```
34
+ */
35
+ export declare class Storage {
36
+ private readonly adapter;
37
+ constructor(adapter: StorageAdapter);
38
+ /**
39
+ * Connect to storage via a connector.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const storage = await Storage.connect(createMemory()).run();
44
+ * ```
45
+ */
46
+ static connect(connector: StorageConnector): Task<Storage, StorageConnectionFailed>;
47
+ /**
48
+ * Release the connection back to its source (e.g., pool).
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * await storage.close().run();
53
+ * ```
54
+ */
55
+ close(): Task<void, StorageCloseFailed>;
56
+ /**
57
+ * Put an object into storage.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * await storage.put("image.png", imageBytes, { contentType: "image/png" }).run();
62
+ * ```
63
+ */
64
+ put(key: string, body: BodyInput, options?: PutOptions): Task<void, StoragePutFailed>;
65
+ /**
66
+ * Get an object from storage.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * const object = await storage.get("file.txt").run();
71
+ * const text = await new Response(object.body).text();
72
+ * ```
73
+ */
74
+ get(key: string): Task<StorageObject, StorageGetFailed | StorageObjectNotFound>;
75
+ /**
76
+ * Delete an object from storage.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * await storage.delete("old-file.txt").run();
81
+ * ```
82
+ */
83
+ delete(key: string): Task<void, StorageDeleteFailed>;
84
+ /**
85
+ * Get metadata for an object without fetching the body.
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * const metadata = await storage.head("file.txt").run();
90
+ * console.log(metadata.size, metadata.contentType);
91
+ * ```
92
+ */
93
+ head(key: string): Task<StorageMetadata, StorageHeadFailed | StorageObjectNotFound>;
94
+ /**
95
+ * List objects in storage with optional prefix.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * const { successes, errors } = await storage.list("images/")
100
+ * .withConcurrency(10)
101
+ * .map(async (entry) => await processImage(entry))
102
+ * .tapErr((err) => console.error("Failed:", err))
103
+ * .partition();
104
+ * ```
105
+ */
106
+ list(prefix?: string): Source<StorageEntry, StorageListFailed>;
107
+ }
108
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBAAa,OAAO;IACN,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,cAAc;IAEpD;;;;;;;OAOG;IACH,MAAM,CAAC,OAAO,CACZ,SAAS,EAAE,gBAAgB,GAC1B,IAAI,CAAC,OAAO,EAAE,uBAAuB,CAAC;IAazC;;;;;;;OAOG;IACH,KAAK,IAAI,IAAI,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAavC;;;;;;;OAOG;IACH,GAAG,CACD,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,SAAS,EACf,OAAO,CAAC,EAAE,UAAU,GACnB,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAc/B;;;;;;;;OAQG;IACH,GAAG,CACD,GAAG,EAAE,MAAM,GACV,IAAI,CAAC,aAAa,EAAE,gBAAgB,GAAG,qBAAqB,CAAC;IAiBhE;;;;;;;OAOG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAcpD;;;;;;;;OAQG;IACH,IAAI,CACF,GAAG,EAAE,MAAM,GACV,IAAI,CAAC,eAAe,EAAE,iBAAiB,GAAG,qBAAqB,CAAC;IAiBnE;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,YAAY,EAAE,iBAAiB,CAAC;CAkB/D"}
package/esm/storage.js ADDED
@@ -0,0 +1,184 @@
1
+ import { Source, Task } from "anabranch";
2
+ import { StorageCloseFailed, StorageConnectionFailed, StorageDeleteFailed, StorageGetFailed, StorageHeadFailed, StorageListFailed, StorageObjectNotFound, StoragePutFailed, } from "./errors.js";
3
+ /**
4
+ * Storage wrapper with Task/Stream semantics for error-tolerant object operations.
5
+ *
6
+ * @example Basic put/get operations
7
+ * ```ts
8
+ * import { Storage, createMemory } from "@anabranch/storage";
9
+ *
10
+ * const connector = createMemory();
11
+ * const storage = await Storage.connect(connector).run();
12
+ *
13
+ * await storage.put("hello.txt", "Hello, World!").run();
14
+ * const object = await storage.get("hello.txt").run();
15
+ * ```
16
+ *
17
+ * @example Stream listing objects
18
+ * ```ts
19
+ * const { successes, errors } = await storage.list("users/")
20
+ * .withConcurrency(5)
21
+ * .map(async (entry) => await processEntry(entry))
22
+ * .tapErr((err) => console.error("Failed:", err))
23
+ * .partition();
24
+ * ```
25
+ *
26
+ * @example With retry and timeout
27
+ * ```ts
28
+ * await storage.put("important.txt", data)
29
+ * .retry({ attempts: 3, delay: (attempt) => 100 * Math.pow(2, attempt) })
30
+ * .timeout(30_000)
31
+ * .run();
32
+ * ```
33
+ */
34
+ export class Storage {
35
+ constructor(adapter) {
36
+ Object.defineProperty(this, "adapter", {
37
+ enumerable: true,
38
+ configurable: true,
39
+ writable: true,
40
+ value: adapter
41
+ });
42
+ }
43
+ /**
44
+ * Connect to storage via a connector.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const storage = await Storage.connect(createMemory()).run();
49
+ * ```
50
+ */
51
+ static connect(connector) {
52
+ return Task.of(async () => {
53
+ try {
54
+ return new Storage(await connector.connect());
55
+ }
56
+ catch (error) {
57
+ throw new StorageConnectionFailed(error instanceof Error ? error.message : String(error), error);
58
+ }
59
+ });
60
+ }
61
+ /**
62
+ * Release the connection back to its source (e.g., pool).
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * await storage.close().run();
67
+ * ```
68
+ */
69
+ close() {
70
+ return Task.of(async () => {
71
+ try {
72
+ await this.adapter.close();
73
+ }
74
+ catch (error) {
75
+ throw new StorageCloseFailed(error instanceof Error ? error.message : String(error), error);
76
+ }
77
+ });
78
+ }
79
+ /**
80
+ * Put an object into storage.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * await storage.put("image.png", imageBytes, { contentType: "image/png" }).run();
85
+ * ```
86
+ */
87
+ put(key, body, options) {
88
+ return Task.of(async () => {
89
+ try {
90
+ await this.adapter.put(key, body, options);
91
+ }
92
+ catch (error) {
93
+ throw new StoragePutFailed(key, error instanceof Error ? error.message : String(error), error);
94
+ }
95
+ });
96
+ }
97
+ /**
98
+ * Get an object from storage.
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * const object = await storage.get("file.txt").run();
103
+ * const text = await new Response(object.body).text();
104
+ * ```
105
+ */
106
+ get(key) {
107
+ return Task.of(async () => {
108
+ try {
109
+ return await this.adapter.get(key);
110
+ }
111
+ catch (error) {
112
+ if (error instanceof StorageObjectNotFound) {
113
+ throw error;
114
+ }
115
+ throw new StorageGetFailed(key, error instanceof Error ? error.message : String(error), error);
116
+ }
117
+ });
118
+ }
119
+ /**
120
+ * Delete an object from storage.
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * await storage.delete("old-file.txt").run();
125
+ * ```
126
+ */
127
+ delete(key) {
128
+ return Task.of(async () => {
129
+ try {
130
+ await this.adapter.delete(key);
131
+ }
132
+ catch (error) {
133
+ throw new StorageDeleteFailed(key, error instanceof Error ? error.message : String(error), error);
134
+ }
135
+ });
136
+ }
137
+ /**
138
+ * Get metadata for an object without fetching the body.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * const metadata = await storage.head("file.txt").run();
143
+ * console.log(metadata.size, metadata.contentType);
144
+ * ```
145
+ */
146
+ head(key) {
147
+ return Task.of(async () => {
148
+ try {
149
+ return await this.adapter.head(key);
150
+ }
151
+ catch (error) {
152
+ if (error instanceof StorageObjectNotFound) {
153
+ throw error;
154
+ }
155
+ throw new StorageHeadFailed(key, error instanceof Error ? error.message : String(error), error);
156
+ }
157
+ });
158
+ }
159
+ /**
160
+ * List objects in storage with optional prefix.
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * const { successes, errors } = await storage.list("images/")
165
+ * .withConcurrency(10)
166
+ * .map(async (entry) => await processImage(entry))
167
+ * .tapErr((err) => console.error("Failed:", err))
168
+ * .partition();
169
+ * ```
170
+ */
171
+ list(prefix) {
172
+ const adapter = this.adapter;
173
+ return Source.from(async function* () {
174
+ try {
175
+ for await (const entry of adapter.list(prefix)) {
176
+ yield entry;
177
+ }
178
+ }
179
+ catch (error) {
180
+ throw new StorageListFailed(prefix, error instanceof Error ? error.message : String(error), error);
181
+ }
182
+ });
183
+ }
184
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@anabranch/storage",
3
+ "version": "0.1.0",
4
+ "description": "TODO: Add description",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/frodi-karlsson/anabranch.git"
8
+ },
9
+ "license": "MIT",
10
+ "bugs": {
11
+ "url": "https://github.com/frodi-karlsson/anabranch.git"
12
+ },
13
+ "module": "./esm/index.js",
14
+ "exports": {
15
+ ".": {
16
+ "import": "./esm/index.js"
17
+ }
18
+ },
19
+ "scripts": {},
20
+ "dependencies": {
21
+ "anabranch": "^0"
22
+ },
23
+ "_generatedBy": "dnt@dev"
24
+ }