@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.
- package/README.md +9 -9
- package/dist/index.js +21 -45
- 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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
87
|
-
and the [decisions behind it](https://github.com/stowage-js/stowage/tree/@stowage/adapter-memory@0.
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
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.
|
|
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.
|
|
36
|
+
"@stowage/core": "^0.2.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=24"
|