@musallam/aio-libs-local 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,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2025 Adobe. All rights reserved.
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # @musallam/aio-libs-local
2
+
3
+ Local and in-memory stand-ins for [`@adobe/aio-lib-files`](https://github.com/adobe/aio-lib-files) and [`@adobe/aio-lib-state`](https://github.com/adobe/aio-lib-state), so App Builder code can run outside Adobe I/O Runtime (local dev, unit tests, CI) without changing call sites.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add -D @musallam/aio-libs-local
9
+ ```
10
+
11
+ npm and yarn work too:
12
+
13
+ ```bash
14
+ npm i -D @musallam/aio-libs-local
15
+ # yarn add -D @musallam/aio-libs-local
16
+ ```
17
+
18
+ Peer dependencies on the real Adobe libs are optional — you only need them when running against Runtime or parity tests.
19
+
20
+ ## Quick start (recommended): build-time aliasing
21
+
22
+ Keep imports as `@adobe/aio-lib-files` and `@adobe/aio-lib-state`; configure your bundler or test runner to swap them for this package locally. This avoids pulling `@azure/storage-blob` into local bundles.
23
+
24
+ ### Webpack (`aio app build`)
25
+
26
+ ```js
27
+ // webpack.config.js
28
+ const isLocal = process.env.TARGET === "local";
29
+
30
+ module.exports = {
31
+ resolve: {
32
+ alias: isLocal
33
+ ? {
34
+ "@adobe/aio-lib-files": "@musallam/aio-libs-local/files",
35
+ "@adobe/aio-lib-state": "@musallam/aio-libs-local/state",
36
+ }
37
+ : {},
38
+ },
39
+ };
40
+ ```
41
+
42
+ ### Vite
43
+
44
+ ```js
45
+ import { defineConfig } from "vite";
46
+
47
+ export default defineConfig(({ mode }) => ({
48
+ resolve: {
49
+ alias:
50
+ mode === "development"
51
+ ? {
52
+ "@adobe/aio-lib-files": "@musallam/aio-libs-local/files",
53
+ "@adobe/aio-lib-state": "@musallam/aio-libs-local/state",
54
+ }
55
+ : {},
56
+ },
57
+ }));
58
+ ```
59
+
60
+ ### Vitest
61
+
62
+ ```js
63
+ // vitest.config.ts
64
+ import path from "node:path";
65
+ import { defineConfig } from "vitest/config";
66
+
67
+ export default defineConfig({
68
+ test: { environment: "node" },
69
+ resolve: {
70
+ alias: {
71
+ "@adobe/aio-lib-files": path.resolve("node_modules/@musallam/aio-libs-local/dist/files.js"),
72
+ "@adobe/aio-lib-state": path.resolve("node_modules/@musallam/aio-libs-local/dist/state.js"),
73
+ },
74
+ },
75
+ });
76
+ ```
77
+
78
+ ## Quick start (no bundler): `/auto` entry point
79
+
80
+ For plain Node scripts, REPLs, or `ts-node` without a bundler:
81
+
82
+ ```js
83
+ const { Files, State } = require("@musallam/aio-libs-local/auto");
84
+
85
+ const files = await Files.init();
86
+ const state = await State.init();
87
+ ```
88
+
89
+ Detection uses `process.env.__OW_API_KEY` as the Runtime signal. Unset it in `.env.local` to force stubs. Use `forceLocal()` / `forceRuntime()` from the auto module to override in tests.
90
+
91
+ **Do not use `/auto` under webpack, Vite, or esbuild** — the bundler will include both implementations.
92
+
93
+ ## Explicit imports
94
+
95
+ ```js
96
+ const filesLib = process.env.__OW_API_KEY
97
+ ? require("@adobe/aio-lib-files")
98
+ : require("@musallam/aio-libs-local/files");
99
+
100
+ const stateLib = process.env.__OW_API_KEY
101
+ ? require("@adobe/aio-lib-state")
102
+ : require("@musallam/aio-libs-local/state");
103
+ ```
104
+
105
+ ## Local persistence
106
+
107
+ **State** — optional disk backing:
108
+
109
+ ```js
110
+ const state = await require("@musallam/aio-libs-local/state").init({
111
+ persist: "./.aio-state",
112
+ });
113
+ ```
114
+
115
+ **Files** — filesystem root (default: `.aio-files/` in cwd):
116
+
117
+ ```js
118
+ const files = await require("@musallam/aio-libs-local/files").init({
119
+ root: "./.aio-files",
120
+ });
121
+ ```
122
+
123
+ Paths under `public/` are treated as publicly accessible, matching the real Files lib.
124
+
125
+ ## Fidelity gaps
126
+
127
+ | Area | Real lib | This stub |
128
+ | --------------------- | ---------------- | ------------------------------------------- |
129
+ | Presigned URLs | Azure SAS URLs | `file://` absolute paths |
130
+ | State TTL max | 365 days | 30 days (configurable via `MAX_TTL`) |
131
+ | State infinite TTL | Not supported | `ttl: -1` supported locally |
132
+ | Consistency | Eventual (cloud) | Immediate (memory/disk) |
133
+ | Multi-tenant / region | Yes | No |
134
+ | Error messages | Adobe SDK format | Same `code` values; message text may differ |
135
+ | Rate limiting | Yes | No |
136
+
137
+ ## Development
138
+
139
+ This repo uses **pnpm**, [Oxlint](https://oxc.rs/docs/guide/usage/linter.html), and [Oxfmt](https://oxc.rs/docs/guide/usage/formatter.html). Git hooks (via [Husky](https://typicode.github.io/husky/)) run lint-staged on commit and [Commitlint](https://commitlint.js.org/) on commit messages (Conventional Commits).
140
+
141
+ ```bash
142
+ pnpm install # installs deps and sets up git hooks (prepare → husky)
143
+ pnpm run lint # oxlint
144
+ pnpm run fmt # format in place
145
+ pnpm run fmt:check # CI-style format check
146
+ ```
147
+
148
+ ## Testing
149
+
150
+ This package uses [Vitest](https://vitest.dev/). Development uses **pnpm** (`pnpm install` once in the repo root).
151
+
152
+ All tests run **offline** against the local stubs only — they never connect to Azure Blob Storage or OpenWhisk / Adobe State, even if you have cloud credentials in your environment.
153
+
154
+ ```bash
155
+ pnpm test # full suite (unit, contract, bundler smoke)
156
+ pnpm run test:unit # unit + contract tests (no bundler smoke)
157
+ pnpm run test:bundler # Vite + esbuild alias smoke tests
158
+ pnpm run test:watch # watch mode
159
+ ```
160
+
161
+ npm equivalents: `npm test`, `npm run test:unit`, etc.
162
+
163
+ ## Releasing
164
+
165
+ Releases are automated with [semantic-release](https://semantic-release.gitbook.io/) on push to `main` / `master` (see [.github/workflows/release.yml](.github/workflows/release.yml)).
166
+
167
+ 1. Use [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `chore:`, etc.) — enforced by commitlint on every commit.
168
+ 2. Merge to `main`; the Release workflow runs tests, then:
169
+ - Bumps `package.json` version
170
+ - Updates `CHANGELOG.md`
171
+ - Creates a [GitHub Release](https://github.com/ahmed-musallam/aio-libs-local/releases)
172
+ - Publishes `@musallam/aio-libs-local` to npm with [provenance](https://docs.npmjs.com/generating-provenance-statements)
173
+
174
+ ### npm trusted publishing (one-time setup)
175
+
176
+ No `NPM_TOKEN` secret is required. Configure [trusted publishing](https://docs.npmjs.com/trusted-publishers) on npm for the package (or the `@musallam` scope):
177
+
178
+ | Field | Value |
179
+ | ----------------- | --------------------------------------------------- |
180
+ | Provider | GitHub Actions |
181
+ | Repository | `ahmed-musallam/aio-libs-local` |
182
+ | Workflow filename | `release.yml` |
183
+ | Environment | _(leave empty unless you use a GitHub Environment)_ |
184
+
185
+ Ensure the `@musallam` scope exists on npm and allows public packages (`publishConfig.access` is `public`). The workflow sets `id-token: write` for OIDC; do **not** set `registry-url` on `setup-node` in the release workflow.
186
+
187
+ ## License
188
+
189
+ Apache-2.0
package/dist/auto.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type * as RealFiles from "@adobe/aio-lib-files";
2
+ import type * as RealState from "@adobe/aio-lib-state";
3
+ export declare function forceLocal(): void;
4
+ export declare function forceRuntime(): void;
5
+ export declare function resetForce(): void;
6
+ /** Lazy proxy so forceLocal/forceRuntime apply on each access */
7
+ export declare const Files: typeof RealFiles;
8
+ export declare const State: typeof RealState;
package/dist/auto.js ADDED
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ /* aio-libs-local stub */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.State = exports.Files = void 0;
5
+ exports.forceLocal = forceLocal;
6
+ exports.forceRuntime = forceRuntime;
7
+ exports.resetForce = resetForce;
8
+ let forceMode = null;
9
+ function forceLocal() {
10
+ forceMode = "local";
11
+ }
12
+ function forceRuntime() {
13
+ forceMode = "runtime";
14
+ }
15
+ function resetForce() {
16
+ forceMode = null;
17
+ }
18
+ function inRuntime() {
19
+ if (forceMode === "local")
20
+ return false;
21
+ if (forceMode === "runtime")
22
+ return true;
23
+ return !!process.env.__OW_API_KEY;
24
+ }
25
+ /**
26
+ * Runtime-switching entry point. Suitable for plain Node environments
27
+ * (scripts, tests without a bundler, REPLs).
28
+ *
29
+ * WARNING: For bundled apps (webpack, Vite, esbuild), prefer build-time
30
+ * aliasing — see README "Bundler integration".
31
+ */
32
+ function loadFiles() {
33
+ return inRuntime() ? require("@adobe/aio-lib-files") : require("./files");
34
+ }
35
+ function loadState() {
36
+ return inRuntime() ? require("@adobe/aio-lib-state") : require("./state");
37
+ }
38
+ /** Lazy proxy so forceLocal/forceRuntime apply on each access */
39
+ exports.Files = new Proxy({}, {
40
+ get(_target, prop) {
41
+ return loadFiles()[prop];
42
+ },
43
+ });
44
+ exports.State = new Proxy({}, {
45
+ get(_target, prop) {
46
+ return loadState()[prop];
47
+ },
48
+ });
49
+ module.exports = { Files: exports.Files, State: exports.State, forceLocal, forceRuntime, resetForce };
@@ -0,0 +1,16 @@
1
+ export declare class AioLibError extends Error {
2
+ code: string;
3
+ sdk?: string;
4
+ sdkDetails?: unknown;
5
+ constructor(code: string, message: string, sdkDetails?: unknown);
6
+ }
7
+ export declare const ERROR_BAD_REQUEST = "ERROR_BAD_REQUEST";
8
+ export declare const ERROR_BAD_ARGUMENT = "ERROR_BAD_ARGUMENT";
9
+ export declare const ERROR_BAD_FILE_NAME = "ERROR_BAD_FILE_NAME";
10
+ export declare const ERROR_FILE_NOT_EXISTS = "ERROR_FILE_NOT_EXISTS";
11
+ export declare const ERROR_PAYLOAD_TOO_LARGE = "ERROR_PAYLOAD_TOO_LARGE";
12
+ export declare const ERROR_UNAUTHORIZED = "ERROR_UNAUTHORIZED";
13
+ export declare const ERROR_BAD_FILE_TYPE = "ERROR_BAD_FILE_TYPE";
14
+ export declare const ERROR_OUT_OF_RANGE = "ERROR_OUT_OF_RANGE";
15
+ export declare const ERROR_NOT_IMPLEMENTED = "ERROR_NOT_IMPLEMENTED";
16
+ export declare function throwError(code: string, message: string, sdkDetails?: unknown): never;
package/dist/errors.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /* aio-libs-local stub */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ERROR_NOT_IMPLEMENTED = exports.ERROR_OUT_OF_RANGE = exports.ERROR_BAD_FILE_TYPE = exports.ERROR_UNAUTHORIZED = exports.ERROR_PAYLOAD_TOO_LARGE = exports.ERROR_FILE_NOT_EXISTS = exports.ERROR_BAD_FILE_NAME = exports.ERROR_BAD_ARGUMENT = exports.ERROR_BAD_REQUEST = exports.AioLibError = void 0;
5
+ exports.throwError = throwError;
6
+ class AioLibError extends Error {
7
+ code;
8
+ sdk;
9
+ sdkDetails;
10
+ constructor(code, message, sdkDetails) {
11
+ super(message);
12
+ this.name = "AioLibError";
13
+ this.code = code;
14
+ this.sdk = "AioLibsLocal";
15
+ this.sdkDetails = sdkDetails;
16
+ }
17
+ }
18
+ exports.AioLibError = AioLibError;
19
+ exports.ERROR_BAD_REQUEST = "ERROR_BAD_REQUEST";
20
+ exports.ERROR_BAD_ARGUMENT = "ERROR_BAD_ARGUMENT";
21
+ exports.ERROR_BAD_FILE_NAME = "ERROR_BAD_FILE_NAME";
22
+ exports.ERROR_FILE_NOT_EXISTS = "ERROR_FILE_NOT_EXISTS";
23
+ exports.ERROR_PAYLOAD_TOO_LARGE = "ERROR_PAYLOAD_TOO_LARGE";
24
+ exports.ERROR_UNAUTHORIZED = "ERROR_UNAUTHORIZED";
25
+ exports.ERROR_BAD_FILE_TYPE = "ERROR_BAD_FILE_TYPE";
26
+ exports.ERROR_OUT_OF_RANGE = "ERROR_OUT_OF_RANGE";
27
+ exports.ERROR_NOT_IMPLEMENTED = "ERROR_NOT_IMPLEMENTED";
28
+ function throwError(code, message, sdkDetails) {
29
+ throw new AioLibError(code, message, sdkDetails);
30
+ }
@@ -0,0 +1,55 @@
1
+ export declare const STUB_MARKER = "aio-libs-local stub";
2
+ export interface InitOptions {
3
+ /** Root directory for file storage. Defaults to `.aio-files` in cwd. */
4
+ root?: string;
5
+ }
6
+ export interface RemoteFileProperties {
7
+ name: string;
8
+ creationTime: Date;
9
+ lastModified: Date;
10
+ contentLength: number;
11
+ contentType: string;
12
+ isDirectory: boolean;
13
+ isPublic: boolean;
14
+ url: string;
15
+ internalUrl: string;
16
+ }
17
+ export interface Files {
18
+ list(filePath?: string): Promise<RemoteFileProperties[]>;
19
+ read(filePath: string, options?: {
20
+ position?: number;
21
+ length?: number;
22
+ }): Promise<Buffer>;
23
+ write(filePath: string, content: string | Buffer | NodeJS.ReadableStream): Promise<number>;
24
+ delete(filePath: string, options?: {
25
+ progressCallback?: (f: string) => void;
26
+ }): Promise<string[]>;
27
+ copy(srcPath: string, destPath: string, options?: {
28
+ localSrc?: boolean;
29
+ localDest?: boolean;
30
+ noOverwrite?: boolean;
31
+ progressCallback?: (src: string, dest: string) => void;
32
+ }): Promise<Record<string, string>>;
33
+ getProperties(filePath: string): Promise<RemoteFileProperties>;
34
+ generatePresignURL(filePath: string, options: {
35
+ expiryInSeconds: number;
36
+ permissions?: string;
37
+ urlType?: string;
38
+ }): Promise<string>;
39
+ createReadStream(filePath: string, options?: {
40
+ position?: number;
41
+ length?: number;
42
+ }): Promise<NodeJS.ReadableStream>;
43
+ createWriteStream(filePath: string): Promise<NodeJS.WritableStream>;
44
+ revokeAllPresignURLs(): Promise<void>;
45
+ }
46
+ export declare function init(options?: InitOptions): Promise<Files>;
47
+ export declare const FilePermissions: {
48
+ READ: string;
49
+ WRITE: string;
50
+ DELETE: string;
51
+ };
52
+ export declare const UrlType: {
53
+ external: string;
54
+ internal: string;
55
+ };
package/dist/files.js ADDED
@@ -0,0 +1,399 @@
1
+ "use strict";
2
+ /* aio-libs-local stub */
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.UrlType = exports.FilePermissions = exports.STUB_MARKER = void 0;
41
+ exports.init = init;
42
+ const fs = __importStar(require("node:fs"));
43
+ const fsp = __importStar(require("node:fs/promises"));
44
+ const path = __importStar(require("node:path"));
45
+ const promises_1 = require("node:stream/promises");
46
+ const node_stream_1 = require("node:stream");
47
+ const mime_types_1 = __importDefault(require("mime-types"));
48
+ const errors_1 = require("./errors");
49
+ const path_utils_1 = require("./path-utils");
50
+ exports.STUB_MARKER = "aio-libs-local stub";
51
+ const PUBLIC_PREFIX = "public";
52
+ function assertNoTraversal(filePath) {
53
+ const segments = filePath.replace(/\\/g, "/").split("/");
54
+ if (segments.some((s) => s === "..")) {
55
+ (0, errors_1.throwError)(errors_1.ERROR_BAD_FILE_NAME, `path traversal not allowed: ${filePath}`, { filePath });
56
+ }
57
+ }
58
+ function normalizeRemotePath(filePath) {
59
+ assertNoTraversal(filePath);
60
+ let res = filePath;
61
+ if (!res.startsWith("/"))
62
+ res = "/" + res;
63
+ res = path.posix.normalize(res);
64
+ while (res.startsWith("/"))
65
+ res = res.slice(1);
66
+ return res;
67
+ }
68
+ function isRemoteRoot(filePath) {
69
+ return filePath === "";
70
+ }
71
+ function isRemotePublic(filePath) {
72
+ return filePath === PUBLIC_PREFIX || filePath.startsWith(PUBLIC_PREFIX + "/");
73
+ }
74
+ function isRemoteDirectory(filePath) {
75
+ return filePath.endsWith("/") || filePath === "" || filePath === PUBLIC_PREFIX;
76
+ }
77
+ function throwIfRemoteDirectory(filePath, details) {
78
+ if (isRemoteDirectory(filePath)) {
79
+ (0, errors_1.throwError)(errors_1.ERROR_BAD_FILE_TYPE, `${filePath} is a directory but should be a file`, details);
80
+ }
81
+ }
82
+ class LocalFiles {
83
+ root;
84
+ presignUrls = new Set();
85
+ constructor(root) {
86
+ this.root = path.resolve(root);
87
+ }
88
+ resolveLocal(normalizedPath) {
89
+ if (normalizedPath.includes("\0")) {
90
+ (0, errors_1.throwError)(errors_1.ERROR_BAD_FILE_NAME, `invalid file path: ${normalizedPath}`, {
91
+ filePath: normalizedPath,
92
+ });
93
+ }
94
+ const segments = normalizedPath.split("/").filter(Boolean);
95
+ for (const seg of segments) {
96
+ if (seg === "..") {
97
+ (0, errors_1.throwError)(errors_1.ERROR_BAD_FILE_NAME, `path traversal not allowed: ${normalizedPath}`, {
98
+ filePath: normalizedPath,
99
+ });
100
+ }
101
+ }
102
+ const local = path.join(this.root, ...segments);
103
+ const resolved = path.resolve(local);
104
+ if (!(0, path_utils_1.isInsideRoot)(resolved, this.root)) {
105
+ (0, errors_1.throwError)(errors_1.ERROR_BAD_FILE_NAME, `path escapes storage root: ${normalizedPath}`, {
106
+ filePath: normalizedPath,
107
+ });
108
+ }
109
+ return resolved;
110
+ }
111
+ fileUrl(normalizedPath) {
112
+ const local = this.resolveLocal(normalizedPath);
113
+ return (0, path_utils_1.toFileUrl)(local);
114
+ }
115
+ async propsFromPath(normalizedPath, localPath) {
116
+ let stat;
117
+ try {
118
+ stat = await fsp.stat(localPath);
119
+ }
120
+ catch (e) {
121
+ if (e?.code === "ENOENT") {
122
+ (0, errors_1.throwError)(errors_1.ERROR_FILE_NOT_EXISTS, `file \`${normalizedPath}\` does not exist`, {
123
+ filePath: normalizedPath,
124
+ });
125
+ }
126
+ throw e;
127
+ }
128
+ const isDirectory = stat.isDirectory();
129
+ const url = this.fileUrl(normalizedPath);
130
+ return {
131
+ name: normalizedPath,
132
+ creationTime: stat.birthtime,
133
+ lastModified: stat.mtime,
134
+ contentLength: isDirectory ? 0 : stat.size,
135
+ contentType: isDirectory
136
+ ? "application/x-directory"
137
+ : mime_types_1.default.lookup(normalizedPath) || "application/octet-stream",
138
+ isDirectory,
139
+ isPublic: isRemotePublic(normalizedPath),
140
+ url,
141
+ internalUrl: url,
142
+ };
143
+ }
144
+ async collectFiles(normalizedDir) {
145
+ const results = [];
146
+ const localDir = this.resolveLocal(normalizedDir);
147
+ const walk = async (dir, prefix) => {
148
+ let entries;
149
+ try {
150
+ entries = await fsp.readdir(dir, { withFileTypes: true });
151
+ }
152
+ catch (e) {
153
+ if (e?.code === "ENOENT")
154
+ return;
155
+ throw e;
156
+ }
157
+ for (const ent of entries) {
158
+ const rel = prefix ? `${prefix}/${ent.name}` : ent.name;
159
+ const full = path.join(dir, ent.name);
160
+ if (ent.isDirectory()) {
161
+ const norm = rel + "/";
162
+ results.push(await this.propsFromPath(norm, full));
163
+ await walk(full, rel);
164
+ }
165
+ else if (ent.isFile()) {
166
+ results.push(await this.propsFromPath(rel, full));
167
+ }
168
+ }
169
+ };
170
+ if (isRemoteRoot(normalizedDir)) {
171
+ await fsp.mkdir(this.root, { recursive: true });
172
+ const top = await fsp.readdir(this.root, { withFileTypes: true });
173
+ for (const ent of top) {
174
+ const full = path.join(this.root, ent.name);
175
+ if (ent.isDirectory()) {
176
+ const remoteName = ent.name === PUBLIC_PREFIX ? PUBLIC_PREFIX : `${ent.name}/`;
177
+ results.push(await this.propsFromPath(remoteName, full));
178
+ await walk(full, ent.name === PUBLIC_PREFIX ? PUBLIC_PREFIX : ent.name);
179
+ }
180
+ else {
181
+ results.push(await this.propsFromPath(ent.name, full));
182
+ }
183
+ }
184
+ return results;
185
+ }
186
+ await walk(localDir, normalizedDir.replace(/\/$/, ""));
187
+ return results;
188
+ }
189
+ async list(filePath = "") {
190
+ const normalized = normalizeRemotePath(filePath);
191
+ if (isRemoteDirectory(normalized)) {
192
+ return this.collectFiles(normalized);
193
+ }
194
+ try {
195
+ const info = await this.getProperties(normalized);
196
+ return [info];
197
+ }
198
+ catch (e) {
199
+ if (e?.code === errors_1.ERROR_FILE_NOT_EXISTS)
200
+ return [];
201
+ throw e;
202
+ }
203
+ }
204
+ async read(filePath, options = {}) {
205
+ const normalized = normalizeRemotePath(filePath);
206
+ const details = { filePath, options };
207
+ throwIfRemoteDirectory(normalized, details);
208
+ const local = this.resolveLocal(normalized);
209
+ let stat;
210
+ try {
211
+ stat = await fsp.stat(local);
212
+ }
213
+ catch (e) {
214
+ if (e?.code === "ENOENT") {
215
+ (0, errors_1.throwError)(errors_1.ERROR_FILE_NOT_EXISTS, `file \`${normalized}\` does not exist`, details);
216
+ }
217
+ throw e;
218
+ }
219
+ const position = options.position ?? 0;
220
+ if (position > stat.size) {
221
+ (0, errors_1.throwError)(errors_1.ERROR_OUT_OF_RANGE, `options.position ${position} out of range for file ${normalized}`, details);
222
+ }
223
+ const length = options.length ?? stat.size - position;
224
+ const fd = await fsp.open(local, "r");
225
+ try {
226
+ const buf = Buffer.alloc(Math.min(length, stat.size - position));
227
+ await fd.read(buf, 0, buf.length, position);
228
+ return buf;
229
+ }
230
+ finally {
231
+ await fd.close();
232
+ }
233
+ }
234
+ async write(filePath, content) {
235
+ const normalized = normalizeRemotePath(filePath);
236
+ const details = { filePath: normalized };
237
+ throwIfRemoteDirectory(normalized, details);
238
+ const local = this.resolveLocal(normalized);
239
+ await fsp.mkdir(path.dirname(local), { recursive: true });
240
+ if (typeof content === "string") {
241
+ await fsp.writeFile(local, content, "utf8");
242
+ return Buffer.byteLength(content, "utf8");
243
+ }
244
+ if (Buffer.isBuffer(content)) {
245
+ await fsp.writeFile(local, content);
246
+ return content.length;
247
+ }
248
+ const ws = fs.createWriteStream(local);
249
+ await (0, promises_1.pipeline)(content, ws);
250
+ const stat = await fsp.stat(local);
251
+ return stat.size;
252
+ }
253
+ async delete(filePath, options = {}) {
254
+ const elements = await this.list(filePath);
255
+ const deleted = [];
256
+ for (const fp of elements) {
257
+ const name = fp.name;
258
+ const local = this.resolveLocal(name);
259
+ await fsp.rm(local, { recursive: true, force: true });
260
+ if (options.progressCallback)
261
+ options.progressCallback(name);
262
+ deleted.push(name);
263
+ }
264
+ return deleted;
265
+ }
266
+ async getProperties(filePath) {
267
+ const normalized = normalizeRemotePath(filePath);
268
+ return this.propsFromPath(normalized, this.resolveLocal(normalized));
269
+ }
270
+ async createReadStream(filePath, options = {}) {
271
+ const buf = await this.read(filePath, options);
272
+ return node_stream_1.Readable.from(buf);
273
+ }
274
+ async createWriteStream(filePath) {
275
+ const normalized = normalizeRemotePath(filePath);
276
+ throwIfRemoteDirectory(normalized, { filePath });
277
+ const local = this.resolveLocal(normalized);
278
+ await fsp.mkdir(path.dirname(local), { recursive: true });
279
+ return fs.createWriteStream(local);
280
+ }
281
+ async copy(srcPath, destPath, options = {}) {
282
+ if (options.localSrc && options.localDest) {
283
+ (0, errors_1.throwError)(errors_1.ERROR_BAD_FILE_TYPE, "local to local copy is not supported", {
284
+ srcPath,
285
+ destPath,
286
+ options,
287
+ });
288
+ }
289
+ const normalizeSrc = (p) => options.localSrc ? path.resolve(p) : this.resolveLocal(normalizeRemotePath(p));
290
+ const srcIsDir = options.localSrc
291
+ ? (await fsp.stat(normalizeSrc(srcPath)).catch(() => null))?.isDirectory()
292
+ : isRemoteDirectory(normalizeRemotePath(srcPath));
293
+ const srcFiles = [];
294
+ if (options.localSrc) {
295
+ const srcLocal = normalizeSrc(srcPath);
296
+ const stat = await fsp.stat(srcLocal).catch((e) => {
297
+ if (e?.code === "ENOENT") {
298
+ (0, errors_1.throwError)(errors_1.ERROR_FILE_NOT_EXISTS, `file \`${srcPath}\` does not exist`, { srcPath });
299
+ }
300
+ throw e;
301
+ });
302
+ if (stat.isFile()) {
303
+ srcFiles.push(srcLocal);
304
+ }
305
+ else {
306
+ const walk = async (dir) => {
307
+ for (const ent of await fsp.readdir(dir, { withFileTypes: true })) {
308
+ const full = path.join(dir, ent.name);
309
+ if (ent.isDirectory())
310
+ await walk(full);
311
+ else if (ent.isFile())
312
+ srcFiles.push(full);
313
+ }
314
+ };
315
+ await walk(srcLocal);
316
+ }
317
+ }
318
+ else {
319
+ const listed = await this.list(srcPath);
320
+ if (listed.length === 0) {
321
+ (0, errors_1.throwError)(errors_1.ERROR_FILE_NOT_EXISTS, `file \`${srcPath}\` does not exist`, { srcPath });
322
+ }
323
+ srcFiles.push(...listed.filter((f) => !f.isDirectory).map((f) => f.name));
324
+ }
325
+ const mapping = {};
326
+ const normSrc = options.localSrc ? srcPath : normalizeRemotePath(srcPath);
327
+ const normDest = options.localDest ? destPath : normalizeRemotePath(destPath);
328
+ for (const srcFile of srcFiles) {
329
+ let destFile;
330
+ if (options.localSrc) {
331
+ const rel = path.relative(normalizeSrc(srcPath), srcFile);
332
+ const base = normDest.endsWith("/") || (srcIsDir && !options.localDest)
333
+ ? path.posix.join(normalizeRemotePath(normDest), rel.split(path.sep).join("/"))
334
+ : normalizeRemotePath(normDest);
335
+ destFile = base;
336
+ }
337
+ else {
338
+ const rel = path.posix.relative(normSrc.replace(/\/$/, ""), srcFile);
339
+ destFile =
340
+ srcIsDir && !normDest.endsWith("/")
341
+ ? path.posix.join(normDest + "/", rel)
342
+ : srcIsDir
343
+ ? path.posix.join(normDest.endsWith("/") ? normDest : normDest + "/", rel)
344
+ : normDest;
345
+ }
346
+ if (options.noOverwrite) {
347
+ try {
348
+ if (options.localDest) {
349
+ await fsp.access(path.resolve(destFile));
350
+ continue;
351
+ }
352
+ else {
353
+ await this.getProperties(destFile);
354
+ continue;
355
+ }
356
+ }
357
+ catch {
358
+ // does not exist — proceed
359
+ }
360
+ }
361
+ if (options.localSrc && !options.localDest) {
362
+ const content = await fsp.readFile(srcFile);
363
+ await this.write(destFile, content);
364
+ }
365
+ else if (!options.localSrc && options.localDest) {
366
+ const content = await this.read(srcFile);
367
+ await fsp.mkdir(path.dirname(path.resolve(destFile)), { recursive: true });
368
+ await fsp.writeFile(path.resolve(destFile), content);
369
+ }
370
+ else if (!options.localSrc && !options.localDest) {
371
+ const content = await this.read(srcFile);
372
+ await this.write(destFile, content);
373
+ }
374
+ mapping[srcFile] = destFile;
375
+ if (options.progressCallback)
376
+ options.progressCallback(srcFile, destFile);
377
+ }
378
+ return mapping;
379
+ }
380
+ async generatePresignURL(filePath, options) {
381
+ const normalized = normalizeRemotePath(filePath);
382
+ throwIfRemoteDirectory(normalized, { filePath, options });
383
+ await this.getProperties(normalized);
384
+ const url = this.fileUrl(normalized);
385
+ this.presignUrls.add(url);
386
+ return url;
387
+ }
388
+ async revokeAllPresignURLs() {
389
+ this.presignUrls.clear();
390
+ }
391
+ }
392
+ async function init(options = {}) {
393
+ const root = path.resolve(options.root ?? path.join(process.cwd(), ".aio-files"));
394
+ await fsp.mkdir(root, { recursive: true });
395
+ return new LocalFiles(root);
396
+ }
397
+ exports.FilePermissions = { READ: "r", WRITE: "w", DELETE: "d" };
398
+ exports.UrlType = { external: "external", internal: "internal" };
399
+ module.exports = { init, FilePermissions: exports.FilePermissions, UrlType: exports.UrlType, STUB_MARKER: exports.STUB_MARKER };
@@ -0,0 +1,4 @@
1
+ export * as Files from "./files";
2
+ export * as State from "./state";
3
+ export * as Auto from "./auto";
4
+ export * from "./errors";
package/dist/index.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ /* aio-libs-local stub */
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
37
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.Auto = exports.State = exports.Files = void 0;
41
+ exports.Files = __importStar(require("./files"));
42
+ exports.State = __importStar(require("./state"));
43
+ exports.Auto = __importStar(require("./auto"));
44
+ __exportStar(require("./errors"), exports);
@@ -0,0 +1,4 @@
1
+ /** True when `resolved` is the storage root or a path under it (Windows-safe). */
2
+ export declare function isInsideRoot(resolved: string, root: string): boolean;
3
+ /** Platform-correct file:// URL for a local filesystem path. */
4
+ export declare function toFileUrl(localPath: string): string;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isInsideRoot = isInsideRoot;
37
+ exports.toFileUrl = toFileUrl;
38
+ const path = __importStar(require("node:path"));
39
+ const node_url_1 = require("node:url");
40
+ /** True when `resolved` is the storage root or a path under it (Windows-safe). */
41
+ function isInsideRoot(resolved, root) {
42
+ const normalizedRoot = path.resolve(root);
43
+ const normalizedResolved = path.resolve(resolved);
44
+ const relative = path.relative(normalizedRoot, normalizedResolved);
45
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
46
+ }
47
+ /** Platform-correct file:// URL for a local filesystem path. */
48
+ function toFileUrl(localPath) {
49
+ return (0, node_url_1.pathToFileURL)(path.resolve(localPath)).href;
50
+ }
@@ -0,0 +1,42 @@
1
+ export declare const MAX_TTL = 2592000;
2
+ export interface InitOptions {
3
+ /** Persist state to a directory on disk. If omitted, state is in-memory. */
4
+ persist?: string;
5
+ /** Namespace prefix for keys (for parity testing). Optional. */
6
+ namespace?: string;
7
+ }
8
+ export interface PutOptions {
9
+ /** TTL in seconds. Default 86400 (24h). Max 2592000 (30d). -1 = no expiry. */
10
+ ttl?: number;
11
+ }
12
+ export interface StateValue {
13
+ value: any;
14
+ expiration?: string;
15
+ }
16
+ export interface State {
17
+ get(key: string): Promise<StateValue | undefined>;
18
+ put(key: string, value: any, options?: PutOptions): Promise<string>;
19
+ delete(key: string): Promise<string | null>;
20
+ deleteAll(options: {
21
+ match: string;
22
+ }): Promise<{
23
+ keys: number;
24
+ }>;
25
+ any(options?: {
26
+ match?: string;
27
+ }): Promise<boolean>;
28
+ stats(options?: {
29
+ match?: string;
30
+ }): Promise<{
31
+ keys: number;
32
+ bytesKeys: number;
33
+ bytesValues: number;
34
+ }>;
35
+ list(options?: {
36
+ match?: string;
37
+ countHint?: number;
38
+ }): AsyncGenerator<{
39
+ keys: string[];
40
+ }>;
41
+ }
42
+ export declare function init(initOptions?: InitOptions): Promise<State>;
package/dist/state.js ADDED
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ /* aio-libs-local stub */
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.MAX_TTL = void 0;
38
+ exports.init = init;
39
+ const fs = __importStar(require("node:fs/promises"));
40
+ const path = __importStar(require("node:path"));
41
+ const errors_1 = require("./errors");
42
+ exports.MAX_TTL = 2592000; // 30 days per local stub spec
43
+ const DEFAULT_TTL = 86400;
44
+ const MAX_VALUE_BYTES = 1024 * 1024; // 1MB
45
+ const KEY_REGEX = /^[a-zA-Z0-9_\-.]{1,1024}$/;
46
+ const MATCH_REGEX = /^[a-zA-Z0-9_\-.]{1,1024}$|^[a-zA-Z0-9_\-.]{0,1023}\*$/;
47
+ const stores = new Map();
48
+ function storeKey(options) {
49
+ return options.persist ?? options.namespace ?? "__memory__";
50
+ }
51
+ function getStore(options) {
52
+ const key = storeKey(options);
53
+ if (!stores.has(key))
54
+ stores.set(key, new Map());
55
+ return stores.get(key);
56
+ }
57
+ function namespacedKey(key, options) {
58
+ return options.namespace ? `${options.namespace}:${key}` : key;
59
+ }
60
+ function validateKey(key) {
61
+ if (!KEY_REGEX.test(key)) {
62
+ (0, errors_1.throwError)(errors_1.ERROR_BAD_REQUEST, `invalid key: ${key}`, { key });
63
+ }
64
+ }
65
+ function validateMatch(match) {
66
+ if (!MATCH_REGEX.test(match)) {
67
+ (0, errors_1.throwError)(errors_1.ERROR_BAD_REQUEST, `invalid match pattern: ${match}`, { match });
68
+ }
69
+ }
70
+ function validateTtl(ttl) {
71
+ if (ttl === -1)
72
+ return;
73
+ if (ttl < 0) {
74
+ (0, errors_1.throwError)(errors_1.ERROR_BAD_REQUEST, "ttl must be -1 (no expiry) or a non-negative number", { ttl });
75
+ }
76
+ if (ttl > exports.MAX_TTL) {
77
+ (0, errors_1.throwError)(errors_1.ERROR_BAD_REQUEST, `ttl must be <= ${exports.MAX_TTL} seconds (30 days)`, { ttl });
78
+ }
79
+ }
80
+ function validateValue(value) {
81
+ const serialized = typeof value === "string" ? value : JSON.stringify(value);
82
+ const bytes = Buffer.byteLength(serialized, "utf8");
83
+ if (bytes > MAX_VALUE_BYTES) {
84
+ throw new errors_1.AioLibError(errors_1.ERROR_PAYLOAD_TOO_LARGE, "key, value or request payload is too large", {
85
+ bytes,
86
+ max: MAX_VALUE_BYTES,
87
+ });
88
+ }
89
+ }
90
+ function globToRegExp(pattern) {
91
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
92
+ return new RegExp(`^${escaped}$`);
93
+ }
94
+ function matchesPattern(key, pattern) {
95
+ if (!pattern)
96
+ return true;
97
+ validateMatch(pattern);
98
+ return globToRegExp(pattern).test(key);
99
+ }
100
+ function isExpired(entry) {
101
+ return entry.expiresAt !== null && entry.expiresAt < Date.now();
102
+ }
103
+ function entryToStateValue(entry) {
104
+ return {
105
+ value: entry.value,
106
+ expiration: entry.expiresAt === null ? undefined : new Date(entry.expiresAt).toISOString(),
107
+ };
108
+ }
109
+ function debounce(fn, ms) {
110
+ let timer = null;
111
+ return () => {
112
+ if (timer)
113
+ clearTimeout(timer);
114
+ timer = setTimeout(fn, ms);
115
+ };
116
+ }
117
+ async function loadFromDisk(persistDir, store) {
118
+ const filePath = path.join(persistDir, "state.json");
119
+ try {
120
+ const raw = await fs.readFile(filePath, "utf8");
121
+ const data = JSON.parse(raw);
122
+ store.clear();
123
+ for (const [k, v] of Object.entries(data)) {
124
+ store.set(k, v);
125
+ }
126
+ }
127
+ catch (e) {
128
+ if (e?.code !== "ENOENT")
129
+ throw e;
130
+ }
131
+ }
132
+ async function saveToDisk(persistDir, store) {
133
+ await fs.mkdir(persistDir, { recursive: true });
134
+ const filePath = path.join(persistDir, "state.json");
135
+ const obj = {};
136
+ for (const [k, v] of store.entries()) {
137
+ obj[k] = v;
138
+ }
139
+ await fs.writeFile(filePath, JSON.stringify(obj), "utf8");
140
+ }
141
+ async function init(initOptions = {}) {
142
+ const store = getStore(initOptions);
143
+ if (initOptions.persist) {
144
+ await loadFromDisk(initOptions.persist, store);
145
+ }
146
+ const persist = initOptions.persist
147
+ ? debounce(() => {
148
+ void saveToDisk(initOptions.persist, store);
149
+ }, 100)
150
+ : () => { };
151
+ const resolveKey = (key) => namespacedKey(key, initOptions);
152
+ const toLogicalKey = (storedKey) => initOptions.namespace ? storedKey.slice(initOptions.namespace.length + 1) : storedKey;
153
+ return {
154
+ async get(key) {
155
+ validateKey(key);
156
+ const nk = resolveKey(key);
157
+ const entry = store.get(nk);
158
+ if (!entry)
159
+ return undefined;
160
+ if (isExpired(entry)) {
161
+ store.delete(nk);
162
+ persist();
163
+ return undefined;
164
+ }
165
+ return entryToStateValue(entry);
166
+ },
167
+ async put(key, value, opts = {}) {
168
+ validateKey(key);
169
+ validateValue(value);
170
+ let ttl = opts.ttl ?? DEFAULT_TTL;
171
+ if (ttl === 0)
172
+ ttl = DEFAULT_TTL;
173
+ validateTtl(ttl);
174
+ const expiresAt = ttl === -1 ? null : Date.now() + ttl * 1000;
175
+ const nk = resolveKey(key);
176
+ store.set(nk, { value, expiresAt });
177
+ persist();
178
+ return key;
179
+ },
180
+ async delete(key) {
181
+ validateKey(key);
182
+ const nk = resolveKey(key);
183
+ if (!store.has(nk))
184
+ return null;
185
+ store.delete(nk);
186
+ persist();
187
+ return key;
188
+ },
189
+ async deleteAll(options) {
190
+ if (!options?.match) {
191
+ (0, errors_1.throwError)(errors_1.ERROR_BAD_REQUEST, "deleteAll requires { match } option", { options });
192
+ }
193
+ validateMatch(options.match);
194
+ let count = 0;
195
+ for (const [k, entry] of Array.from(store.entries())) {
196
+ const logicalKey = toLogicalKey(k);
197
+ if (matchesPattern(logicalKey, options.match) && !isExpired(entry)) {
198
+ store.delete(k);
199
+ count++;
200
+ }
201
+ }
202
+ persist();
203
+ return { keys: count };
204
+ },
205
+ async any(options = {}) {
206
+ for (const [k, entry] of store.entries()) {
207
+ const logicalKey = toLogicalKey(k);
208
+ if (matchesPattern(logicalKey, options.match) && !isExpired(entry)) {
209
+ return true;
210
+ }
211
+ }
212
+ return false;
213
+ },
214
+ async stats(options = {}) {
215
+ let keys = 0;
216
+ let bytesKeys = 0;
217
+ let bytesValues = 0;
218
+ for (const [k, entry] of store.entries()) {
219
+ const logicalKey = toLogicalKey(k);
220
+ if (!matchesPattern(logicalKey, options.match) || isExpired(entry))
221
+ continue;
222
+ keys++;
223
+ bytesKeys += Buffer.byteLength(logicalKey, "utf8");
224
+ const serialized = typeof entry.value === "string" ? entry.value : JSON.stringify(entry.value);
225
+ bytesValues += Buffer.byteLength(serialized, "utf8");
226
+ }
227
+ return { keys, bytesKeys, bytesValues };
228
+ },
229
+ list(options = {}) {
230
+ return (async function* () {
231
+ const keys = [];
232
+ for (const [k, entry] of store.entries()) {
233
+ const logicalKey = toLogicalKey(k);
234
+ if (matchesPattern(logicalKey, options.match) && !isExpired(entry)) {
235
+ keys.push(logicalKey);
236
+ }
237
+ }
238
+ yield { keys };
239
+ })();
240
+ },
241
+ };
242
+ }
243
+ // CommonJS export shape matching @adobe/aio-lib-state
244
+ module.exports = { init, MAX_TTL: exports.MAX_TTL };
package/package.json ADDED
@@ -0,0 +1,118 @@
1
+ {
2
+ "name": "@musallam/aio-libs-local",
3
+ "version": "0.1.0",
4
+ "description": "Local/in-memory stand-ins for @adobe/aio-lib-files and @adobe/aio-lib-state",
5
+ "keywords": [
6
+ "adobe-io",
7
+ "app-builder",
8
+ "local-dev",
9
+ "openwhisk",
10
+ "testing"
11
+ ],
12
+ "homepage": "https://github.com/ahmed-musallam/aio-libs-local#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/ahmed-musallam/aio-libs-local/issues"
15
+ },
16
+ "license": "Apache-2.0",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/ahmed-musallam/aio-libs-local.git"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "main": "dist/index.js",
27
+ "types": "dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js",
32
+ "require": "./dist/index.js"
33
+ },
34
+ "./files": {
35
+ "types": "./dist/files.d.ts",
36
+ "import": "./dist/files.js",
37
+ "require": "./dist/files.js"
38
+ },
39
+ "./state": {
40
+ "types": "./dist/state.d.ts",
41
+ "import": "./dist/state.js",
42
+ "require": "./dist/state.js"
43
+ },
44
+ "./auto": {
45
+ "types": "./dist/auto.d.ts",
46
+ "import": "./dist/auto.js",
47
+ "require": "./dist/auto.js"
48
+ }
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "scripts": {
54
+ "build": "tsc",
55
+ "test": "pnpm run build && vitest run",
56
+ "test:unit": "pnpm run build && vitest run --exclude 'test/bundler/**'",
57
+ "test:bundler": "pnpm run build && vitest run test/bundler",
58
+ "test:watch": "vitest",
59
+ "lint": "oxlint .",
60
+ "lint:fix": "oxlint --fix .",
61
+ "fmt": "oxfmt .",
62
+ "fmt:check": "oxfmt --check .",
63
+ "prepare": "husky",
64
+ "prepublishOnly": "pnpm run build"
65
+ },
66
+ "dependencies": {
67
+ "mime-types": "3.0.2"
68
+ },
69
+ "devDependencies": {
70
+ "@commitlint/cli": "21.0.1",
71
+ "@commitlint/config-conventional": "21.0.1",
72
+ "@semantic-release/changelog": "6.0.3",
73
+ "@semantic-release/commit-analyzer": "13.0.1",
74
+ "@semantic-release/exec": "7.1.0",
75
+ "@semantic-release/git": "10.0.1",
76
+ "@semantic-release/github": "12.0.8",
77
+ "@semantic-release/npm": "13.1.5",
78
+ "@semantic-release/release-notes-generator": "14.1.1",
79
+ "@types/mime-types": "3.0.1",
80
+ "@types/node": "25.9.1",
81
+ "esbuild": "0.28.0",
82
+ "husky": "9.1.7",
83
+ "lint-staged": "17.0.5",
84
+ "oxfmt": "0.51.0",
85
+ "oxlint": "1.66.0",
86
+ "semantic-release": "25.0.3",
87
+ "typescript": "6.0.3",
88
+ "vite": "8.0.14",
89
+ "vitest": "4.1.7"
90
+ },
91
+ "peerDependencies": {
92
+ "@adobe/aio-lib-files": "*",
93
+ "@adobe/aio-lib-state": "*"
94
+ },
95
+ "peerDependenciesMeta": {
96
+ "@adobe/aio-lib-files": {
97
+ "optional": true
98
+ },
99
+ "@adobe/aio-lib-state": {
100
+ "optional": true
101
+ }
102
+ },
103
+ "lint-staged": {
104
+ "*.{ts,js,mjs,cjs}": [
105
+ "oxfmt",
106
+ "oxlint --fix"
107
+ ]
108
+ },
109
+ "engines": {
110
+ "node": ">=20"
111
+ },
112
+ "packageManager": "pnpm@10.30.3",
113
+ "pnpm": {
114
+ "onlyBuiltDependencies": [
115
+ "esbuild"
116
+ ]
117
+ }
118
+ }