@stowage/adapter-memory 0.1.0 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +9 -9
  2. package/dist/index.js +21 -45
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -26,21 +26,21 @@ console.log(object.stat.contentType, object.stat.userMetadata, await object.text
26
26
 
27
27
  ## Runtimes
28
28
 
29
- Node 24 and later, Bun, Deno and `workerd` at the compatibility date `2026-09-01`. CI last ran green on Bun 1.4.2 and Deno 2.9.6.
29
+ Node 24 and later, Bun, Deno and `workerd` at the compatibility date `2026-09-01` without Node APIs. CI last ran green on Bun 1.4.2 and Deno 2.9.6.
30
30
 
31
- The bundle measures 3.3 kB minified and gzipped, `@stowage/core` included.
31
+ The bundle measures 3.6 kB minified and gzipped, `@stowage/core` included.
32
32
 
33
33
  ## Limits
34
34
 
35
35
  - `presignedUrls` is not declared: `MemoryStorage` has neither `presignGet` nor `presignPut`
36
- ([spec 4.9](https://github.com/stowage-js/stowage/blob/@stowage/adapter-memory@0.1.0/docs/spec.md#49-capabilities)).
36
+ ([spec 4.9](https://github.com/stowage-js/stowage/blob/@stowage/adapter-memory@0.2.0/docs/spec.md#49-capabilities)).
37
37
  - Every object is held whole in memory, with no size limit of its own, so memory grows with what
38
- the storage holds ([spec 5](https://github.com/stowage-js/stowage/blob/@stowage/adapter-memory@0.1.0/docs/spec.md#5-stowageadapter-memory)).
38
+ the storage holds ([spec 5](https://github.com/stowage-js/stowage/blob/@stowage/adapter-memory@0.2.0/docs/spec.md#5-stowageadapter-memory)).
39
39
 
40
40
  ## Notes
41
41
 
42
42
  `put` takes no `Blob`. A caller holding one passes its stream
43
- ([spec 4.2](https://github.com/stowage-js/stowage/blob/@stowage/adapter-memory@0.1.0/docs/spec.md#42-bodies)):
43
+ ([spec 4.2](https://github.com/stowage-js/stowage/blob/@stowage/adapter-memory@0.2.0/docs/spec.md#42-bodies)):
44
44
 
45
45
  ```ts
46
46
  import { memoryStorage } from "@stowage/adapter-memory";
@@ -52,7 +52,7 @@ await storage.put("report.csv", blob.stream(), { contentType: blob.type });
52
52
  ```
53
53
 
54
54
  stowage reports no progress
55
- ([spec 11](https://github.com/stowage-js/stowage/blob/@stowage/adapter-memory@0.1.0/docs/spec.md#11-non-goals)).
55
+ ([spec 12](https://github.com/stowage-js/stowage/blob/@stowage/adapter-memory@0.2.0/docs/spec.md#12-non-goals)).
56
56
  A caller who wants it counts the bytes on their way into `put`:
57
57
 
58
58
  ```ts
@@ -81,10 +81,10 @@ await storage.put("fixture.json", response.body.pipeThrough(countBytes(console.l
81
81
 
82
82
  ## Specification
83
83
 
84
- [`docs/spec.md` at `@stowage/adapter-memory@0.1.0`](https://github.com/stowage-js/stowage/blob/@stowage/adapter-memory@0.1.0/docs/spec.md#5-stowageadapter-memory)
84
+ [`docs/spec.md` at `@stowage/adapter-memory@0.2.0`](https://github.com/stowage-js/stowage/blob/@stowage/adapter-memory@0.2.0/docs/spec.md#5-stowageadapter-memory)
85
85
  is the contract: a caller may rely on what it states and on nothing else this package happens to
86
- export. The [terms it uses](https://github.com/stowage-js/stowage/blob/@stowage/adapter-memory@0.1.0/CONTEXT.md)
87
- and the [decisions behind it](https://github.com/stowage-js/stowage/tree/@stowage/adapter-memory@0.1.0/docs/adr)
86
+ export. The [terms it uses](https://github.com/stowage-js/stowage/blob/@stowage/adapter-memory@0.2.0/CONTEXT.md)
87
+ and the [decisions behind it](https://github.com/stowage-js/stowage/tree/@stowage/adapter-memory@0.2.0/docs/adr)
88
88
  are at the same tag.
89
89
 
90
90
  ## License
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { StorageError, invalidKeyReason } from "@stowage/core";
1
+ import { StorageError, checkUserMetadata, invalidKeyReason, lastByteOf, rangeBoundsRefusal, rangeStartRefusal } from "@stowage/core";
2
2
  //#region src/bytes.ts
3
3
  async function readBody(body, signal) {
4
4
  if (isStream(body)) return await readStream(body, signal);
@@ -210,25 +210,24 @@ function pseudoDirectoryOf(key, { prefix, delimiter }) {
210
210
  //#region src/range.ts
211
211
  /** Refuses bounds the spec does not allow, before the key is looked up (spec 4.3). */
212
212
  function requireRange(range) {
213
- if (range === void 0) return;
214
- const { start, end } = range;
215
- if (!isOffset(start) || end !== void 0 && (!isOffset(end) || end < start)) throw optionError("range", "takes two whole numbers from zero up, `start` at most `end`", "get");
213
+ const refusal = rangeBoundsRefusal(range);
214
+ if (refusal !== void 0) throw memoryError({
215
+ ...refusal,
216
+ operation: "get",
217
+ attempts: 0
218
+ });
216
219
  }
217
220
  /** The bytes the range names, both ends inclusive, clipped to what the object holds. */
218
221
  function sliceRange(bytes, range, key) {
219
222
  if (range === void 0) return bytes;
220
- if (range.start >= bytes.byteLength) throw memoryError({
221
- code: "InvalidRequest",
222
- message: `The range starts beyond the ${bytes.byteLength} bytes under the key ${JSON.stringify(key)}`,
223
+ const refusal = rangeStartRefusal(range, bytes.byteLength, key);
224
+ if (refusal !== void 0) throw memoryError({
225
+ ...refusal,
223
226
  operation: "get",
224
227
  key,
225
228
  attempts: 1
226
229
  });
227
- const end = Math.min(range.end ?? bytes.byteLength - 1, bytes.byteLength - 1);
228
- return bytes.subarray(range.start, end + 1);
229
- }
230
- function isOffset(value) {
231
- return Number.isInteger(value) && value >= 0;
230
+ return bytes.subarray(range.start, lastByteOf(range, bytes.byteLength) + 1);
232
231
  }
233
232
  //#endregion
234
233
  //#region src/stored-object.ts
@@ -275,44 +274,20 @@ function createStoredObject(stat, bytes) {
275
274
  }
276
275
  //#endregion
277
276
  //#region src/user-metadata.ts
278
- /** Spec 4.3 bounds the set at 2 KB of the header bytes it costs once it is encoded. */
279
- const headerByteLimit = 2048;
280
- /** The characters RFC 9110 allows in a field name, which is what a metadata key is. */
281
- const httpToken = /^[!#$%&'*+.^_`|~\dA-Za-z-]+$/;
282
- /** What travels in a header field as it stands, so it costs one byte per character. */
283
- const printableAscii = /^[\x20-\x7e]*$/;
284
- /** `=?UTF-8?B?` and the `?=` that closes it. */
285
- const encodedWordOverhead = 12;
286
- const utf8 = new TextEncoder();
287
277
  /**
288
278
  * The metadata as it is held: keys folded to lower case, as a header field name is.
289
- * Rejects with `InvalidRequest` before anything is written (spec 4.3).
279
+ * Rejects before anything is written, in the order of spec 4.3, which reads the refusals
280
+ * off what the storage declares.
290
281
  */
291
- function readUserMetadata(userMetadata, key) {
292
- const held = Object.create(null);
293
- let headerBytes = 0;
294
- for (const [name, value] of Object.entries(userMetadata ?? {})) {
295
- if (!httpToken.test(name)) throw refusal(`The user metadata key ${JSON.stringify(name)} is no ASCII HTTP token`, key);
296
- const folded = name.toLowerCase();
297
- if (folded in held) throw refusal(`The user metadata key ${JSON.stringify(folded)} is given more than once`, key);
298
- held[folded] = value;
299
- headerBytes += folded.length + encodedValueBytes(value);
300
- }
301
- if (headerBytes > headerByteLimit) throw refusal(`The user metadata is ${headerBytes} encoded header bytes, above the limit of ${headerByteLimit}`, key);
302
- return Object.freeze(held);
303
- }
304
- function encodedValueBytes(value) {
305
- if (printableAscii.test(value)) return value.length;
306
- return encodedWordOverhead + 4 * Math.ceil(utf8.encode(value).length / 3);
307
- }
308
- function refusal(message, key) {
309
- return memoryError({
310
- code: "InvalidRequest",
311
- message,
282
+ function readUserMetadata(userMetadata, key, capabilities) {
283
+ const check = checkUserMetadata(userMetadata, capabilities);
284
+ if ("refusal" in check) throw memoryError({
285
+ ...check.refusal,
312
286
  operation: "put",
313
287
  key,
314
288
  attempts: 0
315
289
  });
290
+ return check.held;
316
291
  }
317
292
  //#endregion
318
293
  //#region src/index.ts
@@ -323,7 +298,8 @@ const defaultContentType = "application/octet-stream";
323
298
  const memoryCapabilities = Object.freeze([
324
299
  "keyBytesPreserved",
325
300
  "rangeReads",
326
- "userMetadata"
301
+ "userMetadata",
302
+ "userMetadataTokenKeys"
327
303
  ]);
328
304
  var InMemoryStorage = class {
329
305
  provider = "memory";
@@ -353,7 +329,7 @@ var InMemoryStorage = class {
353
329
  try {
354
330
  requireKey(key, "writable", "put");
355
331
  requireKnownOptions(options, putOptionKeys, "put");
356
- return readUserMetadata(options?.userMetadata, key);
332
+ return readUserMetadata(options?.userMetadata, key, this.capabilities);
357
333
  } catch (refusal) {
358
334
  try {
359
335
  await cancelBody(body, refusal);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stowage/adapter-memory",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "stowage adapter holding a storage in process memory, and the implementation a third-party adapter is read against.",
5
5
  "keywords": [
6
6
  "adapter",
@@ -33,7 +33,7 @@
33
33
  "provenance": true
34
34
  },
35
35
  "dependencies": {
36
- "@stowage/core": "^0.1.0"
36
+ "@stowage/core": "^0.2.0"
37
37
  },
38
38
  "engines": {
39
39
  "node": ">=24"