@graffiti-garden/implementation-decentralized 0.1.0 → 0.1.2
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/dist/1-services/2-dids.d.ts +1 -0
- package/dist/1-services/2-dids.d.ts.map +1 -1
- package/dist/1-services/3-storage-buckets.d.ts +5 -0
- package/dist/1-services/3-storage-buckets.d.ts.map +1 -1
- package/dist/1-services/4-inboxes.d.ts +2 -1
- package/dist/1-services/4-inboxes.d.ts.map +1 -1
- package/dist/browser/index.js +7 -7
- package/dist/browser/index.js.map +3 -3
- package/dist/cjs/1-services/2-dids-tests.js +53 -0
- package/dist/cjs/1-services/2-dids-tests.js.map +3 -3
- package/dist/cjs/1-services/2-dids.js +14 -4
- package/dist/cjs/1-services/2-dids.js.map +2 -2
- package/dist/cjs/1-services/3-storage-buckets-tests.js +179 -0
- package/dist/cjs/1-services/3-storage-buckets-tests.js.map +3 -3
- package/dist/cjs/1-services/3-storage-buckets.js +91 -0
- package/dist/cjs/1-services/3-storage-buckets.js.map +3 -3
- package/dist/cjs/1-services/4-inboxes-tests.js +20 -0
- package/dist/cjs/1-services/4-inboxes-tests.js.map +3 -3
- package/dist/cjs/1-services/4-inboxes.js +36 -17
- package/dist/cjs/1-services/4-inboxes.js.map +2 -2
- package/dist/cjs/3-protocol/4-graffiti.js +1 -1
- package/dist/cjs/3-protocol/4-graffiti.js.map +1 -1
- package/dist/esm/1-services/2-dids-tests.js +54 -1
- package/dist/esm/1-services/2-dids-tests.js.map +3 -3
- package/dist/esm/1-services/2-dids.js +14 -4
- package/dist/esm/1-services/2-dids.js.map +2 -2
- package/dist/esm/1-services/3-storage-buckets-tests.js +180 -1
- package/dist/esm/1-services/3-storage-buckets-tests.js.map +2 -2
- package/dist/esm/1-services/3-storage-buckets.js +92 -1
- package/dist/esm/1-services/3-storage-buckets.js.map +2 -2
- package/dist/esm/1-services/4-inboxes-tests.js +20 -0
- package/dist/esm/1-services/4-inboxes-tests.js.map +2 -2
- package/dist/esm/1-services/4-inboxes.js +36 -17
- package/dist/esm/1-services/4-inboxes.js.map +2 -2
- package/dist/esm/3-protocol/4-graffiti.js +1 -1
- package/dist/esm/3-protocol/4-graffiti.js.map +1 -1
- package/package.json +1 -1
- package/src/1-services/2-dids-tests.ts +62 -1
- package/src/1-services/2-dids.ts +17 -5
- package/src/1-services/3-storage-buckets-tests.ts +199 -1
- package/src/1-services/3-storage-buckets.ts +129 -1
- package/src/1-services/4-inboxes-tests.ts +25 -0
- package/src/1-services/4-inboxes.ts +47 -19
- package/src/3-protocol/4-graffiti.ts +1 -1
|
@@ -39,6 +39,59 @@ function didTests() {
|
|
|
39
39
|
const result = await dids.resolve(did);
|
|
40
40
|
(0, import_vitest.expect)(result).toHaveProperty("id", did);
|
|
41
41
|
});
|
|
42
|
+
(0, import_vitest.test)("coalesces simultaneous resolutions", async () => {
|
|
43
|
+
const dids2 = new import_dids.DecentralizedIdentifiers();
|
|
44
|
+
const did = "did:web:example.com";
|
|
45
|
+
const found = {
|
|
46
|
+
didDocument: { id: did },
|
|
47
|
+
didDocumentMetadata: {},
|
|
48
|
+
didResolutionMetadata: {}
|
|
49
|
+
};
|
|
50
|
+
let finish;
|
|
51
|
+
const lookup = import_vitest.vi.fn().mockImplementation(
|
|
52
|
+
() => new Promise((resolve) => {
|
|
53
|
+
finish = resolve;
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
Object.defineProperty(dids2, "resolver", { value: { resolve: lookup } });
|
|
57
|
+
const first = dids2.resolve(did);
|
|
58
|
+
const second = dids2.resolve(did);
|
|
59
|
+
(0, import_vitest.expect)(lookup).toHaveBeenCalledTimes(1);
|
|
60
|
+
finish(found);
|
|
61
|
+
await (0, import_vitest.expect)(Promise.all([first, second])).resolves.toEqual([
|
|
62
|
+
found.didDocument,
|
|
63
|
+
found.didDocument
|
|
64
|
+
]);
|
|
65
|
+
lookup.mockResolvedValue(found);
|
|
66
|
+
await (0, import_vitest.expect)(dids2.resolve(did)).resolves.toEqual(found.didDocument);
|
|
67
|
+
(0, import_vitest.expect)(lookup).toHaveBeenCalledTimes(2);
|
|
68
|
+
});
|
|
69
|
+
(0, import_vitest.test)("retries a failed resolution", async () => {
|
|
70
|
+
const dids2 = new import_dids.DecentralizedIdentifiers();
|
|
71
|
+
const did = "did:web:example.com";
|
|
72
|
+
const found = {
|
|
73
|
+
didDocument: { id: did },
|
|
74
|
+
didDocumentMetadata: {},
|
|
75
|
+
didResolutionMetadata: {}
|
|
76
|
+
};
|
|
77
|
+
const lookup = import_vitest.vi.fn().mockResolvedValueOnce({
|
|
78
|
+
didDocument: null,
|
|
79
|
+
didDocumentMetadata: {},
|
|
80
|
+
didResolutionMetadata: { error: "notFound" }
|
|
81
|
+
}).mockResolvedValueOnce(found);
|
|
82
|
+
Object.defineProperty(dids2, "resolver", { value: { resolve: lookup } });
|
|
83
|
+
const failed = await Promise.allSettled([
|
|
84
|
+
dids2.resolve(did),
|
|
85
|
+
dids2.resolve(did)
|
|
86
|
+
]);
|
|
87
|
+
(0, import_vitest.expect)(lookup).toHaveBeenCalledTimes(1);
|
|
88
|
+
(0, import_vitest.expect)(failed.map((result) => result.status)).toEqual([
|
|
89
|
+
"rejected",
|
|
90
|
+
"rejected"
|
|
91
|
+
]);
|
|
92
|
+
await (0, import_vitest.expect)(dids2.resolve(did)).resolves.toEqual(found.didDocument);
|
|
93
|
+
(0, import_vitest.expect)(lookup).toHaveBeenCalledTimes(2);
|
|
94
|
+
});
|
|
42
95
|
});
|
|
43
96
|
}
|
|
44
97
|
//# sourceMappingURL=2-dids-tests.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/1-services/2-dids-tests.ts"],
|
|
4
|
-
"sourcesContent": ["import { describe, expect, test } from \"vitest\";\nimport { DecentralizedIdentifiers } from \"./2-dids\";\n\nexport function didTests() {\n return describe(\"DecentralizedIdentifiers\", () => {\n const dids = new DecentralizedIdentifiers();\n\n test(\"invalid method\", async () => {\n await expect(dids.resolve(\"did:invalid:12345\")).rejects.toThrowError();\n });\n\n test(\"did:web\", async () => {\n const did = \"did:web:identity.foundation\";\n const result = await dids.resolve(did);\n expect(result).toHaveProperty(\"id\", did);\n });\n\n test(\"did:plc\", async () => {\n const did = \"did:plc:44ybard66vv44zksje25o7dz\";\n const result = await dids.resolve(did);\n expect(result).toHaveProperty(\"id\", did);\n });\n });\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import type { DIDResolutionResult, Resolver } from \"did-resolver\";\nimport { describe, expect, test, vi } from \"vitest\";\nimport { DecentralizedIdentifiers } from \"./2-dids\";\n\nexport function didTests() {\n return describe(\"DecentralizedIdentifiers\", () => {\n const dids = new DecentralizedIdentifiers();\n\n test(\"invalid method\", async () => {\n await expect(dids.resolve(\"did:invalid:12345\")).rejects.toThrowError();\n });\n\n test(\"did:web\", async () => {\n const did = \"did:web:identity.foundation\";\n const result = await dids.resolve(did);\n expect(result).toHaveProperty(\"id\", did);\n });\n\n test(\"did:plc\", async () => {\n const did = \"did:plc:44ybard66vv44zksje25o7dz\";\n const result = await dids.resolve(did);\n expect(result).toHaveProperty(\"id\", did);\n });\n\n test(\"coalesces simultaneous resolutions\", async () => {\n const dids = new DecentralizedIdentifiers();\n const did = \"did:web:example.com\";\n const found: DIDResolutionResult = {\n didDocument: { id: did },\n didDocumentMetadata: {},\n didResolutionMetadata: {},\n };\n let finish!: (result: DIDResolutionResult) => void;\n const lookup = vi.fn<Resolver[\"resolve\"]>().mockImplementation(\n () => new Promise((resolve) => { finish = resolve; }),\n );\n Object.defineProperty(dids, \"resolver\", { value: { resolve: lookup } });\n\n const first = dids.resolve(did);\n const second = dids.resolve(did);\n expect(lookup).toHaveBeenCalledTimes(1);\n\n finish(found);\n await expect(Promise.all([first, second])).resolves.toEqual([\n found.didDocument,\n found.didDocument,\n ]);\n\n lookup.mockResolvedValue(found);\n await expect(dids.resolve(did)).resolves.toEqual(found.didDocument);\n expect(lookup).toHaveBeenCalledTimes(2);\n });\n\n test(\"retries a failed resolution\", async () => {\n const dids = new DecentralizedIdentifiers();\n const did = \"did:web:example.com\";\n const found: DIDResolutionResult = {\n didDocument: { id: did },\n didDocumentMetadata: {},\n didResolutionMetadata: {},\n };\n const lookup = vi.fn<Resolver[\"resolve\"]>()\n .mockResolvedValueOnce({\n didDocument: null,\n didDocumentMetadata: {},\n didResolutionMetadata: { error: \"notFound\" },\n })\n .mockResolvedValueOnce(found);\n Object.defineProperty(dids, \"resolver\", { value: { resolve: lookup } });\n\n const failed = await Promise.allSettled([\n dids.resolve(did),\n dids.resolve(did),\n ]);\n expect(lookup).toHaveBeenCalledTimes(1);\n expect(failed.map((result) => result.status)).toEqual([\n \"rejected\",\n \"rejected\",\n ]);\n\n await expect(dids.resolve(did)).resolves.toEqual(found.didDocument);\n expect(lookup).toHaveBeenCalledTimes(2);\n });\n });\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,oBAA2C;AAC3C,kBAAyC;AAElC,SAAS,WAAW;AACzB,aAAO,wBAAS,4BAA4B,MAAM;AAChD,UAAM,OAAO,IAAI,qCAAyB;AAE1C,4BAAK,kBAAkB,YAAY;AACjC,gBAAM,sBAAO,KAAK,QAAQ,mBAAmB,CAAC,EAAE,QAAQ,aAAa;AAAA,IACvE,CAAC;AAED,4BAAK,WAAW,YAAY;AAC1B,YAAM,MAAM;AACZ,YAAM,SAAS,MAAM,KAAK,QAAQ,GAAG;AACrC,gCAAO,MAAM,EAAE,eAAe,MAAM,GAAG;AAAA,IACzC,CAAC;AAED,4BAAK,WAAW,YAAY;AAC1B,YAAM,MAAM;AACZ,YAAM,SAAS,MAAM,KAAK,QAAQ,GAAG;AACrC,gCAAO,MAAM,EAAE,eAAe,MAAM,GAAG;AAAA,IACzC,CAAC;AAED,4BAAK,sCAAsC,YAAY;AACrD,YAAMA,QAAO,IAAI,qCAAyB;AAC1C,YAAM,MAAM;AACZ,YAAM,QAA6B;AAAA,QACjC,aAAa,EAAE,IAAI,IAAI;AAAA,QACvB,qBAAqB,CAAC;AAAA,QACtB,uBAAuB,CAAC;AAAA,MAC1B;AACA,UAAI;AACJ,YAAM,SAAS,iBAAG,GAAwB,EAAE;AAAA,QAC1C,MAAM,IAAI,QAAQ,CAAC,YAAY;AAAE,mBAAS;AAAA,QAAS,CAAC;AAAA,MACtD;AACA,aAAO,eAAeA,OAAM,YAAY,EAAE,OAAO,EAAE,SAAS,OAAO,EAAE,CAAC;AAEtE,YAAM,QAAQA,MAAK,QAAQ,GAAG;AAC9B,YAAM,SAASA,MAAK,QAAQ,GAAG;AAC/B,gCAAO,MAAM,EAAE,sBAAsB,CAAC;AAEtC,aAAO,KAAK;AACZ,gBAAM,sBAAO,QAAQ,IAAI,CAAC,OAAO,MAAM,CAAC,CAAC,EAAE,SAAS,QAAQ;AAAA,QAC1D,MAAM;AAAA,QACN,MAAM;AAAA,MACR,CAAC;AAED,aAAO,kBAAkB,KAAK;AAC9B,gBAAM,sBAAOA,MAAK,QAAQ,GAAG,CAAC,EAAE,SAAS,QAAQ,MAAM,WAAW;AAClE,gCAAO,MAAM,EAAE,sBAAsB,CAAC;AAAA,IACxC,CAAC;AAED,4BAAK,+BAA+B,YAAY;AAC9C,YAAMA,QAAO,IAAI,qCAAyB;AAC1C,YAAM,MAAM;AACZ,YAAM,QAA6B;AAAA,QACjC,aAAa,EAAE,IAAI,IAAI;AAAA,QACvB,qBAAqB,CAAC;AAAA,QACtB,uBAAuB,CAAC;AAAA,MAC1B;AACA,YAAM,SAAS,iBAAG,GAAwB,EACvC,sBAAsB;AAAA,QACrB,aAAa;AAAA,QACb,qBAAqB,CAAC;AAAA,QACtB,uBAAuB,EAAE,OAAO,WAAW;AAAA,MAC7C,CAAC,EACA,sBAAsB,KAAK;AAC9B,aAAO,eAAeA,OAAM,YAAY,EAAE,OAAO,EAAE,SAAS,OAAO,EAAE,CAAC;AAEtE,YAAM,SAAS,MAAM,QAAQ,WAAW;AAAA,QACtCA,MAAK,QAAQ,GAAG;AAAA,QAChBA,MAAK,QAAQ,GAAG;AAAA,MAClB,CAAC;AACD,gCAAO,MAAM,EAAE,sBAAsB,CAAC;AACtC,gCAAO,OAAO,IAAI,CAAC,WAAW,OAAO,MAAM,CAAC,EAAE,QAAQ;AAAA,QACpD;AAAA,QACA;AAAA,MACF,CAAC;AAED,gBAAM,sBAAOA,MAAK,QAAQ,GAAG,CAAC,EAAE,SAAS,QAAQ,MAAM,WAAW;AAClE,gCAAO,MAAM,EAAE,sBAAsB,CAAC;AAAA,IACxC,CAAC;AAAA,EACH,CAAC;AACH;",
|
|
6
|
+
"names": ["dids"]
|
|
7
7
|
}
|
|
@@ -34,17 +34,27 @@ class DecentralizedIdentifiers {
|
|
|
34
34
|
this.methods,
|
|
35
35
|
{ cache: true }
|
|
36
36
|
);
|
|
37
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
37
38
|
async resolve(did) {
|
|
38
39
|
if (!Object.keys(this.methods).some(
|
|
39
40
|
(method) => did.startsWith(`did:${method}:`)
|
|
40
41
|
)) {
|
|
41
42
|
throw new Error(`Unrecognized DID method: ${did}`);
|
|
42
43
|
}
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
|
|
44
|
+
const inFlight = this.inFlight.get(did);
|
|
45
|
+
if (inFlight) return inFlight;
|
|
46
|
+
const resolution = this.resolver.resolve(did).then(({ didDocument }) => {
|
|
47
|
+
if (!didDocument) {
|
|
48
|
+
throw new import_api.GraffitiErrorNotFound(`DID not found: ${did}`);
|
|
49
|
+
}
|
|
50
|
+
return didDocument;
|
|
51
|
+
});
|
|
52
|
+
this.inFlight.set(did, resolution);
|
|
53
|
+
try {
|
|
54
|
+
return await resolution;
|
|
55
|
+
} finally {
|
|
56
|
+
this.inFlight.delete(did);
|
|
46
57
|
}
|
|
47
|
-
return didDocument;
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
//# sourceMappingURL=2-dids.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/1-services/2-dids.ts"],
|
|
4
|
-
"sourcesContent": ["import { GraffitiErrorNotFound } from \"@graffiti-garden/api\";\nimport { Resolver, type DIDDocument, type ResolverRegistry } from \"did-resolver\";\nimport { getResolver as plcResolver } from \"plc-did-resolver\";\nimport { getResolver as webResolver } from \"web-did-resolver\";\n\nexport class DecentralizedIdentifiers {\n protected readonly methods: Record<string, unknown> = {\n ...plcResolver(),\n ...webResolver(),\n };\n\n protected readonly resolver = new Resolver(\n this.methods as unknown as ResolverRegistry,\n { cache: true },\n );\n\n async resolve(did: string): Promise<DIDDocument> {\n if (\n !Object.keys(this.methods).some((method) =>\n did.startsWith(`did:${method}:`),\n )\n ) {\n throw new Error(`Unrecognized DID method: ${did}`);\n }\n\n const
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAsC;AACtC,0BAAkE;AAClE,8BAA2C;AAC3C,8BAA2C;AAEpC,MAAM,yBAAyB;AAAA,EACjB,UAAmC;AAAA,IACpD,OAAG,wBAAAA,aAAY;AAAA,IACf,OAAG,wBAAAC,aAAY;AAAA,EACjB;AAAA,EAEmB,WAAW,IAAI;AAAA,IAChC,KAAK;AAAA,IACL,EAAE,OAAO,KAAK;AAAA,EAChB;AAAA,
|
|
4
|
+
"sourcesContent": ["import { GraffitiErrorNotFound } from \"@graffiti-garden/api\";\nimport { Resolver, type DIDDocument, type ResolverRegistry } from \"did-resolver\";\nimport { getResolver as plcResolver } from \"plc-did-resolver\";\nimport { getResolver as webResolver } from \"web-did-resolver\";\n\nexport class DecentralizedIdentifiers {\n protected readonly methods: Record<string, unknown> = {\n ...plcResolver(),\n ...webResolver(),\n };\n\n protected readonly resolver = new Resolver(\n this.methods as unknown as ResolverRegistry,\n { cache: true },\n );\n\n protected readonly inFlight = new Map<string, Promise<DIDDocument>>();\n\n async resolve(did: string): Promise<DIDDocument> {\n if (\n !Object.keys(this.methods).some((method) =>\n did.startsWith(`did:${method}:`),\n )\n ) {\n throw new Error(`Unrecognized DID method: ${did}`);\n }\n\n const inFlight = this.inFlight.get(did);\n if (inFlight) return inFlight;\n\n const resolution = this.resolver.resolve(did).then(({ didDocument }) => {\n if (!didDocument) {\n throw new GraffitiErrorNotFound(`DID not found: ${did}`);\n }\n return didDocument;\n });\n this.inFlight.set(did, resolution);\n\n try {\n return await resolution;\n } finally {\n this.inFlight.delete(did);\n }\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAsC;AACtC,0BAAkE;AAClE,8BAA2C;AAC3C,8BAA2C;AAEpC,MAAM,yBAAyB;AAAA,EACjB,UAAmC;AAAA,IACpD,OAAG,wBAAAA,aAAY;AAAA,IACf,OAAG,wBAAAC,aAAY;AAAA,EACjB;AAAA,EAEmB,WAAW,IAAI;AAAA,IAChC,KAAK;AAAA,IACL,EAAE,OAAO,KAAK;AAAA,EAChB;AAAA,EAEmB,WAAW,oBAAI,IAAkC;AAAA,EAEpE,MAAM,QAAQ,KAAmC;AAC/C,QACE,CAAC,OAAO,KAAK,KAAK,OAAO,EAAE;AAAA,MAAK,CAAC,WAC/B,IAAI,WAAW,OAAO,MAAM,GAAG;AAAA,IACjC,GACA;AACA,YAAM,IAAI,MAAM,4BAA4B,GAAG,EAAE;AAAA,IACnD;AAEA,UAAM,WAAW,KAAK,SAAS,IAAI,GAAG;AACtC,QAAI,SAAU,QAAO;AAErB,UAAM,aAAa,KAAK,SAAS,QAAQ,GAAG,EAAE,KAAK,CAAC,EAAE,YAAY,MAAM;AACtE,UAAI,CAAC,aAAa;AAChB,cAAM,IAAI,iCAAsB,kBAAkB,GAAG,EAAE;AAAA,MACzD;AACA,aAAO;AAAA,IACT,CAAC;AACD,SAAK,SAAS,IAAI,KAAK,UAAU;AAEjC,QAAI;AACF,aAAO,MAAM;AAAA,IACf,UAAE;AACA,WAAK,SAAS,OAAO,GAAG;AAAA,IAC1B;AAAA,EACF;AACF;",
|
|
6
6
|
"names": ["plcResolver", "webResolver"]
|
|
7
7
|
}
|
|
@@ -25,6 +25,7 @@ var import_vitest = require("vitest");
|
|
|
25
25
|
var import_storage_buckets = require("./3-storage-buckets");
|
|
26
26
|
var import_utilities = require("./utilities");
|
|
27
27
|
var import_api = require("@graffiti-garden/api");
|
|
28
|
+
var import_dag_cbor = require("@ipld/dag-cbor");
|
|
28
29
|
function storageBucketTests(storageBucketEndpoint, storageBucketToken) {
|
|
29
30
|
(0, import_vitest.describe)("Storage buckets", async () => {
|
|
30
31
|
const storageBuckets = new import_storage_buckets.StorageBuckets();
|
|
@@ -71,6 +72,184 @@ function storageBucketTests(storageBucketEndpoint, storageBucketToken) {
|
|
|
71
72
|
storageBuckets.get(storageBucketEndpoint, key, bytes.length - 1)
|
|
72
73
|
).rejects.toThrow();
|
|
73
74
|
});
|
|
75
|
+
(0, import_vitest.test)("overwrites across the small and large storage cutoff", async () => {
|
|
76
|
+
const key = crypto.randomUUID();
|
|
77
|
+
const small = new Uint8Array([1, 2, 3]);
|
|
78
|
+
const large = new Uint8Array(32 * 1024 + 1).fill(42);
|
|
79
|
+
const otherLarge = new Uint8Array(32 * 1024 + 1).fill(43);
|
|
80
|
+
await storageBuckets.put(storageBucketEndpoint, key, large, storageBucketToken);
|
|
81
|
+
(0, import_vitest.expect)(await storageBuckets.get(storageBucketEndpoint, key)).toEqual(large);
|
|
82
|
+
await storageBuckets.put(storageBucketEndpoint, key, otherLarge, storageBucketToken);
|
|
83
|
+
(0, import_vitest.expect)(await storageBuckets.get(storageBucketEndpoint, key)).toEqual(otherLarge);
|
|
84
|
+
await storageBuckets.put(storageBucketEndpoint, key, small, storageBucketToken);
|
|
85
|
+
(0, import_vitest.expect)(await storageBuckets.get(storageBucketEndpoint, key)).toEqual(small);
|
|
86
|
+
await storageBuckets.put(storageBucketEndpoint, key, large, storageBucketToken);
|
|
87
|
+
(0, import_vitest.expect)(await storageBuckets.get(storageBucketEndpoint, key)).toEqual(large);
|
|
88
|
+
await storageBuckets.delete(storageBucketEndpoint, key, storageBucketToken);
|
|
89
|
+
await (0, import_vitest.expect)(storageBuckets.get(storageBucketEndpoint, key)).rejects.toThrow(
|
|
90
|
+
import_api.GraffitiErrorNotFound
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
(0, import_vitest.test)("batch reads arbitrary keys from one bucket", async () => {
|
|
94
|
+
const first = crypto.randomUUID();
|
|
95
|
+
const second = crypto.randomUUID();
|
|
96
|
+
const missing = crypto.randomUUID();
|
|
97
|
+
const small = new Uint8Array([1, 2, 3]);
|
|
98
|
+
const large = new Uint8Array(32 * 1024 + 1).fill(7);
|
|
99
|
+
await storageBuckets.put(storageBucketEndpoint, first, small, storageBucketToken);
|
|
100
|
+
await storageBuckets.put(storageBucketEndpoint, second, large, storageBucketToken);
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetch(`${storageBucketEndpoint}/values`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/cbor" },
|
|
105
|
+
body: (0, import_dag_cbor.encode)({
|
|
106
|
+
keys: [first, missing, second, first],
|
|
107
|
+
maxValueBytes: small.length
|
|
108
|
+
}).slice()
|
|
109
|
+
});
|
|
110
|
+
(0, import_vitest.expect)(response.status).toBe(200);
|
|
111
|
+
const decoded = (0, import_dag_cbor.decode)(await response.arrayBuffer());
|
|
112
|
+
(0, import_vitest.expect)(decoded.results.map(({ status }) => status)).toEqual([200, 404, 413, 200]);
|
|
113
|
+
(0, import_vitest.expect)(decoded.results[0].value).toEqual(small);
|
|
114
|
+
(0, import_vitest.expect)(decoded.results[0].etag).toBeTypeOf("string");
|
|
115
|
+
(0, import_vitest.expect)(decoded.results[3].value).toEqual(small);
|
|
116
|
+
const largeResponse = await fetch(`${storageBucketEndpoint}/values`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: { "Content-Type": "application/cbor" },
|
|
119
|
+
body: (0, import_dag_cbor.encode)({
|
|
120
|
+
keys: [second],
|
|
121
|
+
maxValueBytes: large.length
|
|
122
|
+
}).slice()
|
|
123
|
+
});
|
|
124
|
+
(0, import_vitest.expect)(largeResponse.status).toBe(200);
|
|
125
|
+
const largeDecoded = (0, import_dag_cbor.decode)(await largeResponse.arrayBuffer());
|
|
126
|
+
(0, import_vitest.expect)(largeDecoded.results[0].value).toEqual(large);
|
|
127
|
+
const oversizedResponse = await fetch(`${storageBucketEndpoint}/values`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { "Content-Type": "application/cbor" },
|
|
130
|
+
body: (0, import_dag_cbor.encode)({
|
|
131
|
+
keys: [first, second],
|
|
132
|
+
maxValueBytes: 1024 * 1024
|
|
133
|
+
}).slice()
|
|
134
|
+
});
|
|
135
|
+
(0, import_vitest.expect)(oversizedResponse.status).toBe(400);
|
|
136
|
+
const settled = await Promise.allSettled([
|
|
137
|
+
storageBuckets.get(storageBucketEndpoint, first, small.length),
|
|
138
|
+
storageBuckets.get(storageBucketEndpoint, missing, small.length),
|
|
139
|
+
storageBuckets.get(storageBucketEndpoint, second, small.length)
|
|
140
|
+
]);
|
|
141
|
+
(0, import_vitest.expect)(settled[0]).toEqual({ status: "fulfilled", value: small });
|
|
142
|
+
(0, import_vitest.expect)(settled[1].status).toBe("rejected");
|
|
143
|
+
(0, import_vitest.expect)(settled[1].reason).toBeInstanceOf(import_api.GraffitiErrorNotFound);
|
|
144
|
+
(0, import_vitest.expect)(settled[2].status).toBe("rejected");
|
|
145
|
+
(0, import_vitest.expect)(settled[2].reason).toBeInstanceOf(import_api.GraffitiErrorTooLarge);
|
|
146
|
+
(0, import_vitest.expect)(await storageBuckets.get(storageBucketEndpoint, second)).toEqual(large);
|
|
147
|
+
} finally {
|
|
148
|
+
await storageBuckets.delete(storageBucketEndpoint, first, storageBucketToken);
|
|
149
|
+
await storageBuckets.delete(storageBucketEndpoint, second, storageBucketToken);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
(0, import_vitest.test)("accepts a full batch of maximum-length Unicode keys", async () => {
|
|
153
|
+
const key = "\u754C".repeat(255);
|
|
154
|
+
const body = (0, import_dag_cbor.encode)({
|
|
155
|
+
keys: Array(32).fill(key),
|
|
156
|
+
maxValueBytes: 0
|
|
157
|
+
}).slice();
|
|
158
|
+
(0, import_vitest.expect)(body.byteLength).toBeGreaterThan(16 * 1024);
|
|
159
|
+
const response = await fetch(`${storageBucketEndpoint}/values`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: { "Content-Type": "application/cbor" },
|
|
162
|
+
body
|
|
163
|
+
});
|
|
164
|
+
(0, import_vitest.expect)(response.status).toBe(200);
|
|
165
|
+
const decoded = (0, import_dag_cbor.decode)(await response.arrayBuffer());
|
|
166
|
+
(0, import_vitest.expect)(decoded.results.map(({ status }) => status)).toEqual(Array(32).fill(404));
|
|
167
|
+
});
|
|
168
|
+
(0, import_vitest.test)("concurrent bounded gets share one batch", async () => {
|
|
169
|
+
const keys = Array.from({ length: 3 }, () => crypto.randomUUID());
|
|
170
|
+
const values = keys.map((_, i) => new Uint8Array([i + 1]));
|
|
171
|
+
for (let i = 0; i < keys.length; i++) {
|
|
172
|
+
await storageBuckets.put(storageBucketEndpoint, keys[i], values[i], storageBucketToken);
|
|
173
|
+
}
|
|
174
|
+
const originalFetch = globalThis.fetch;
|
|
175
|
+
const calls = [];
|
|
176
|
+
globalThis.fetch = (input, init) => {
|
|
177
|
+
const request = new Request(input, init);
|
|
178
|
+
calls.push(`${request.method} ${request.url}`);
|
|
179
|
+
return originalFetch(input, init);
|
|
180
|
+
};
|
|
181
|
+
try {
|
|
182
|
+
(0, import_vitest.expect)(await Promise.all(keys.map(
|
|
183
|
+
(key) => storageBuckets.get(storageBucketEndpoint, key, 1)
|
|
184
|
+
))).toEqual(values);
|
|
185
|
+
(0, import_vitest.expect)(calls.filter((call) => call === `POST ${storageBucketEndpoint}/values`)).toHaveLength(1);
|
|
186
|
+
(0, import_vitest.expect)(calls.filter((call) => call.includes("/value/"))).toHaveLength(0);
|
|
187
|
+
} finally {
|
|
188
|
+
globalThis.fetch = originalFetch;
|
|
189
|
+
for (const key of keys) {
|
|
190
|
+
await storageBuckets.delete(storageBucketEndpoint, key, storageBucketToken);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
(0, import_vitest.test)("falls back to GET when a bucket server does not support batches", async () => {
|
|
195
|
+
const keys = Array.from({ length: 2 }, () => crypto.randomUUID());
|
|
196
|
+
const value = new Uint8Array([42]);
|
|
197
|
+
for (const key of keys) {
|
|
198
|
+
await storageBuckets.put(storageBucketEndpoint, key, value, storageBucketToken);
|
|
199
|
+
}
|
|
200
|
+
const originalFetch = globalThis.fetch;
|
|
201
|
+
let batchAttempts = 0;
|
|
202
|
+
globalThis.fetch = (input, init) => {
|
|
203
|
+
const request = new Request(input, init);
|
|
204
|
+
if (request.method === "POST" && request.url === `${storageBucketEndpoint}/values`) {
|
|
205
|
+
batchAttempts++;
|
|
206
|
+
return Promise.resolve(new Response("Not found", { status: 404 }));
|
|
207
|
+
}
|
|
208
|
+
return originalFetch(input, init);
|
|
209
|
+
};
|
|
210
|
+
try {
|
|
211
|
+
const legacyBuckets = new import_storage_buckets.StorageBuckets();
|
|
212
|
+
(0, import_vitest.expect)(await Promise.all(keys.map(
|
|
213
|
+
(key) => legacyBuckets.get(storageBucketEndpoint, key, 1)
|
|
214
|
+
))).toEqual([value, value]);
|
|
215
|
+
(0, import_vitest.expect)(await Promise.all(keys.map(
|
|
216
|
+
(key) => legacyBuckets.get(storageBucketEndpoint, key, 1)
|
|
217
|
+
))).toEqual([value, value]);
|
|
218
|
+
(0, import_vitest.expect)(batchAttempts).toBe(1);
|
|
219
|
+
} finally {
|
|
220
|
+
globalThis.fetch = originalFetch;
|
|
221
|
+
for (const key of keys) {
|
|
222
|
+
await storageBuckets.delete(storageBucketEndpoint, key, storageBucketToken);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
(0, import_vitest.test)("a batch error does not disable batching or fall back to GET", async () => {
|
|
227
|
+
const originalFetch = globalThis.fetch;
|
|
228
|
+
let batchAttempts = 0;
|
|
229
|
+
let getAttempts = 0;
|
|
230
|
+
globalThis.fetch = (input, init) => {
|
|
231
|
+
const request = new Request(input, init);
|
|
232
|
+
if (request.method === "POST") {
|
|
233
|
+
batchAttempts++;
|
|
234
|
+
return Promise.resolve(new Response("Batch limit exceeded", { status: 413 }));
|
|
235
|
+
}
|
|
236
|
+
getAttempts++;
|
|
237
|
+
return originalFetch(input, init);
|
|
238
|
+
};
|
|
239
|
+
try {
|
|
240
|
+
const buckets = new import_storage_buckets.StorageBuckets();
|
|
241
|
+
const readPair = () => Promise.all([
|
|
242
|
+
buckets.get(storageBucketEndpoint, "a", 1),
|
|
243
|
+
buckets.get(storageBucketEndpoint, "b", 1)
|
|
244
|
+
]);
|
|
245
|
+
await (0, import_vitest.expect)(readPair()).rejects.toThrow("Batch limit exceeded");
|
|
246
|
+
await (0, import_vitest.expect)(readPair()).rejects.toThrow("Batch limit exceeded");
|
|
247
|
+
(0, import_vitest.expect)(batchAttempts).toBe(2);
|
|
248
|
+
(0, import_vitest.expect)(getAttempts).toBe(0);
|
|
249
|
+
} finally {
|
|
250
|
+
globalThis.fetch = originalFetch;
|
|
251
|
+
}
|
|
252
|
+
});
|
|
74
253
|
(0, import_vitest.test)("unauthorized", async () => {
|
|
75
254
|
const key = Math.random().toString(36).substring(2, 15);
|
|
76
255
|
const input = "Hello world";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/1-services/3-storage-buckets-tests.ts"],
|
|
4
|
-
"sourcesContent": ["import { describe, expect, test } from \"vitest\";\nimport { StorageBuckets } from \"./3-storage-buckets\";\nimport { GraffitiErrorUnauthorized } from \"./utilities\";\nimport { GraffitiErrorNotFound } from \"@graffiti-garden/api\";\n\nexport function storageBucketTests(\n storageBucketEndpoint: string,\n storageBucketToken: string,\n) {\n describe(\"Storage buckets\", async () => {\n const storageBuckets = new StorageBuckets();\n\n test(\"put, get, delete\", async () => {\n const key = Math.random().toString(36).substring(2, 15);\n const input = \"Hello world\";\n const bytes = new TextEncoder().encode(input);\n\n await expect(\n storageBuckets.get(storageBucketEndpoint, key),\n ).rejects.toThrow(GraffitiErrorNotFound);\n\n await storageBuckets.put(\n storageBucketEndpoint,\n key,\n bytes,\n storageBucketToken,\n );\n\n const resultBytes = await storageBuckets.get(\n storageBucketEndpoint,\n key,\n bytes.length,\n );\n const result = new TextDecoder().decode(resultBytes);\n expect(result).toEqual(input);\n\n await storageBuckets.delete(\n storageBucketEndpoint,\n key,\n storageBucketToken,\n );\n\n await expect(\n storageBuckets.get(storageBucketEndpoint, key),\n ).rejects.toThrow(GraffitiErrorNotFound);\n });\n\n test(\"get with limit less than object\", async () => {\n const key = Math.random().toString(36).substring(2, 15);\n const input = \"Hello world\";\n const bytes = new TextEncoder().encode(input);\n\n await storageBuckets.put(\n storageBucketEndpoint,\n key,\n bytes,\n storageBucketToken,\n );\n\n await expect(\n storageBuckets.get(storageBucketEndpoint, key, bytes.length - 1),\n ).rejects.toThrow();\n });\n\n test(\"unauthorized\", async () => {\n const key = Math.random().toString(36).substring(2, 15);\n const input = \"Hello world\";\n const bytes = new TextEncoder().encode(input);\n\n await expect(\n storageBuckets.put(storageBucketEndpoint, key, bytes, \"invalid-token\"),\n ).rejects.toThrow(GraffitiErrorUnauthorized);\n await expect(\n storageBuckets.delete(storageBucketEndpoint, key, \"invalid-token\"),\n ).rejects.toThrow(GraffitiErrorUnauthorized);\n await expect(\n storageBuckets.export(storageBucketEndpoint, \"invalid-token\").next(),\n ).rejects.toThrow(GraffitiErrorUnauthorized);\n });\n\n test(\"export\", async () => {\n // Put a whole bunch of stuff so the export needs to page\n const keys = new Set<string>();\n for (let i = 0; i < 256; i++) {\n const key = Math.random().toString(36).substring(2, 15);\n keys.add(key);\n\n const input = \"Hello world \" + i;\n const bytes = new TextEncoder().encode(input);\n await storageBuckets.put(\n storageBucketEndpoint,\n key,\n bytes,\n storageBucketToken,\n );\n }\n\n // Export\n const retrievedKeys = new Set<string>();\n const iterator = storageBuckets.export(\n storageBucketEndpoint,\n storageBucketToken,\n );\n for await (const result of iterator) {\n if (keys.has(result.key)) {\n retrievedKeys.add(result.key);\n }\n }\n expect(retrievedKeys.size).toEqual(keys.size);\n\n // Delete all the keys\n for (const key of keys) {\n await storageBuckets.delete(\n storageBucketEndpoint,\n key,\n storageBucketToken,\n );\n }\n }, 1000000);\n });\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAuC;AACvC,6BAA+B;AAC/B,uBAA0C;AAC1C,
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import { describe, expect, test } from \"vitest\";\nimport { StorageBuckets } from \"./3-storage-buckets\";\nimport { GraffitiErrorUnauthorized } from \"./utilities\";\nimport { GraffitiErrorNotFound, GraffitiErrorTooLarge } from \"@graffiti-garden/api\";\nimport { encode as dagCborEncode, decode as dagCborDecode } from \"@ipld/dag-cbor\";\n\nexport function storageBucketTests(\n storageBucketEndpoint: string,\n storageBucketToken: string,\n) {\n describe(\"Storage buckets\", async () => {\n const storageBuckets = new StorageBuckets();\n\n test(\"put, get, delete\", async () => {\n const key = Math.random().toString(36).substring(2, 15);\n const input = \"Hello world\";\n const bytes = new TextEncoder().encode(input);\n\n await expect(\n storageBuckets.get(storageBucketEndpoint, key),\n ).rejects.toThrow(GraffitiErrorNotFound);\n\n await storageBuckets.put(\n storageBucketEndpoint,\n key,\n bytes,\n storageBucketToken,\n );\n\n const resultBytes = await storageBuckets.get(\n storageBucketEndpoint,\n key,\n bytes.length,\n );\n const result = new TextDecoder().decode(resultBytes);\n expect(result).toEqual(input);\n\n await storageBuckets.delete(\n storageBucketEndpoint,\n key,\n storageBucketToken,\n );\n\n await expect(\n storageBuckets.get(storageBucketEndpoint, key),\n ).rejects.toThrow(GraffitiErrorNotFound);\n });\n\n test(\"get with limit less than object\", async () => {\n const key = Math.random().toString(36).substring(2, 15);\n const input = \"Hello world\";\n const bytes = new TextEncoder().encode(input);\n\n await storageBuckets.put(\n storageBucketEndpoint,\n key,\n bytes,\n storageBucketToken,\n );\n\n await expect(\n storageBuckets.get(storageBucketEndpoint, key, bytes.length - 1),\n ).rejects.toThrow();\n });\n\n test(\"overwrites across the small and large storage cutoff\", async () => {\n const key = crypto.randomUUID();\n const small = new Uint8Array([1, 2, 3]);\n const large = new Uint8Array(32 * 1024 + 1).fill(42);\n const otherLarge = new Uint8Array(32 * 1024 + 1).fill(43);\n\n await storageBuckets.put(storageBucketEndpoint, key, large, storageBucketToken);\n expect(await storageBuckets.get(storageBucketEndpoint, key)).toEqual(large);\n\n await storageBuckets.put(storageBucketEndpoint, key, otherLarge, storageBucketToken);\n expect(await storageBuckets.get(storageBucketEndpoint, key)).toEqual(otherLarge);\n\n await storageBuckets.put(storageBucketEndpoint, key, small, storageBucketToken);\n expect(await storageBuckets.get(storageBucketEndpoint, key)).toEqual(small);\n\n await storageBuckets.put(storageBucketEndpoint, key, large, storageBucketToken);\n expect(await storageBuckets.get(storageBucketEndpoint, key)).toEqual(large);\n\n await storageBuckets.delete(storageBucketEndpoint, key, storageBucketToken);\n await expect(storageBuckets.get(storageBucketEndpoint, key)).rejects.toThrow(\n GraffitiErrorNotFound,\n );\n });\n\n test(\"batch reads arbitrary keys from one bucket\", async () => {\n const first = crypto.randomUUID();\n const second = crypto.randomUUID();\n const missing = crypto.randomUUID();\n const small = new Uint8Array([1, 2, 3]);\n const large = new Uint8Array(32 * 1024 + 1).fill(7);\n await storageBuckets.put(storageBucketEndpoint, first, small, storageBucketToken);\n await storageBuckets.put(storageBucketEndpoint, second, large, storageBucketToken);\n try {\n const response = await fetch(`${storageBucketEndpoint}/values`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/cbor\" },\n body: dagCborEncode({\n keys: [first, missing, second, first],\n maxValueBytes: small.length,\n }).slice(),\n });\n expect(response.status).toBe(200);\n const decoded = dagCborDecode(await response.arrayBuffer()) as {\n results: Array<{ status: number; value?: Uint8Array; etag?: string }>;\n };\n expect(decoded.results.map(({ status }) => status)).toEqual([200, 404, 413, 200]);\n expect(decoded.results[0].value).toEqual(small);\n expect(decoded.results[0].etag).toBeTypeOf(\"string\");\n expect(decoded.results[3].value).toEqual(small);\n const largeResponse = await fetch(`${storageBucketEndpoint}/values`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/cbor\" },\n body: dagCborEncode({\n keys: [second],\n maxValueBytes: large.length,\n }).slice(),\n });\n expect(largeResponse.status).toBe(200);\n const largeDecoded = dagCborDecode(await largeResponse.arrayBuffer()) as {\n results: Array<{ status: number; value?: Uint8Array }>;\n };\n expect(largeDecoded.results[0].value).toEqual(large);\n\n const oversizedResponse = await fetch(`${storageBucketEndpoint}/values`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/cbor\" },\n body: dagCborEncode({\n keys: [first, second],\n maxValueBytes: 1024 * 1024,\n }).slice(),\n });\n expect(oversizedResponse.status).toBe(400);\n\n const settled = await Promise.allSettled([\n storageBuckets.get(storageBucketEndpoint, first, small.length),\n storageBuckets.get(storageBucketEndpoint, missing, small.length),\n storageBuckets.get(storageBucketEndpoint, second, small.length),\n ]);\n expect(settled[0]).toEqual({ status: \"fulfilled\", value: small });\n expect(settled[1].status).toBe(\"rejected\");\n expect((settled[1] as PromiseRejectedResult).reason).toBeInstanceOf(GraffitiErrorNotFound);\n expect(settled[2].status).toBe(\"rejected\");\n expect((settled[2] as PromiseRejectedResult).reason).toBeInstanceOf(GraffitiErrorTooLarge);\n expect(await storageBuckets.get(storageBucketEndpoint, second)).toEqual(large);\n } finally {\n await storageBuckets.delete(storageBucketEndpoint, first, storageBucketToken);\n await storageBuckets.delete(storageBucketEndpoint, second, storageBucketToken);\n }\n });\n\n test(\"accepts a full batch of maximum-length Unicode keys\", async () => {\n const key = \"\u754C\".repeat(255);\n const body = dagCborEncode({\n keys: Array(32).fill(key),\n maxValueBytes: 0,\n }).slice();\n expect(body.byteLength).toBeGreaterThan(16 * 1024);\n const response = await fetch(`${storageBucketEndpoint}/values`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/cbor\" },\n body,\n });\n expect(response.status).toBe(200);\n const decoded = dagCborDecode(await response.arrayBuffer()) as {\n results: Array<{ status: number }>;\n };\n expect(decoded.results.map(({ status }) => status)).toEqual(Array(32).fill(404));\n });\n\n test(\"concurrent bounded gets share one batch\", async () => {\n const keys = Array.from({ length: 3 }, () => crypto.randomUUID());\n const values = keys.map((_, i) => new Uint8Array([i + 1]));\n for (let i = 0; i < keys.length; i++) {\n await storageBuckets.put(storageBucketEndpoint, keys[i], values[i], storageBucketToken);\n }\n const originalFetch = globalThis.fetch;\n const calls: string[] = [];\n globalThis.fetch = (input, init) => {\n const request = new Request(input, init);\n calls.push(`${request.method} ${request.url}`);\n return originalFetch(input, init);\n };\n try {\n expect(await Promise.all(keys.map((key) =>\n storageBuckets.get(storageBucketEndpoint, key, 1),\n ))).toEqual(values);\n expect(calls.filter((call) => call === `POST ${storageBucketEndpoint}/values`)).toHaveLength(1);\n expect(calls.filter((call) => call.includes(\"/value/\"))).toHaveLength(0);\n } finally {\n globalThis.fetch = originalFetch;\n for (const key of keys) {\n await storageBuckets.delete(storageBucketEndpoint, key, storageBucketToken);\n }\n }\n });\n\n test(\"falls back to GET when a bucket server does not support batches\", async () => {\n const keys = Array.from({ length: 2 }, () => crypto.randomUUID());\n const value = new Uint8Array([42]);\n for (const key of keys) {\n await storageBuckets.put(storageBucketEndpoint, key, value, storageBucketToken);\n }\n const originalFetch = globalThis.fetch;\n let batchAttempts = 0;\n globalThis.fetch = (input, init) => {\n const request = new Request(input, init);\n if (request.method === \"POST\" && request.url === `${storageBucketEndpoint}/values`) {\n batchAttempts++;\n return Promise.resolve(new Response(\"Not found\", { status: 404 }));\n }\n return originalFetch(input, init);\n };\n try {\n const legacyBuckets = new StorageBuckets();\n expect(await Promise.all(keys.map((key) =>\n legacyBuckets.get(storageBucketEndpoint, key, 1),\n ))).toEqual([value, value]);\n expect(await Promise.all(keys.map((key) =>\n legacyBuckets.get(storageBucketEndpoint, key, 1),\n ))).toEqual([value, value]);\n expect(batchAttempts).toBe(1);\n } finally {\n globalThis.fetch = originalFetch;\n for (const key of keys) {\n await storageBuckets.delete(storageBucketEndpoint, key, storageBucketToken);\n }\n }\n });\n\n test(\"a batch error does not disable batching or fall back to GET\", async () => {\n const originalFetch = globalThis.fetch;\n let batchAttempts = 0;\n let getAttempts = 0;\n globalThis.fetch = (input, init) => {\n const request = new Request(input, init);\n if (request.method === \"POST\") {\n batchAttempts++;\n return Promise.resolve(new Response(\"Batch limit exceeded\", { status: 413 }));\n }\n getAttempts++;\n return originalFetch(input, init);\n };\n try {\n const buckets = new StorageBuckets();\n const readPair = () => Promise.all([\n buckets.get(storageBucketEndpoint, \"a\", 1),\n buckets.get(storageBucketEndpoint, \"b\", 1),\n ]);\n await expect(readPair()).rejects.toThrow(\"Batch limit exceeded\");\n await expect(readPair()).rejects.toThrow(\"Batch limit exceeded\");\n expect(batchAttempts).toBe(2);\n expect(getAttempts).toBe(0);\n } finally {\n globalThis.fetch = originalFetch;\n }\n });\n\n test(\"unauthorized\", async () => {\n const key = Math.random().toString(36).substring(2, 15);\n const input = \"Hello world\";\n const bytes = new TextEncoder().encode(input);\n\n await expect(\n storageBuckets.put(storageBucketEndpoint, key, bytes, \"invalid-token\"),\n ).rejects.toThrow(GraffitiErrorUnauthorized);\n await expect(\n storageBuckets.delete(storageBucketEndpoint, key, \"invalid-token\"),\n ).rejects.toThrow(GraffitiErrorUnauthorized);\n await expect(\n storageBuckets.export(storageBucketEndpoint, \"invalid-token\").next(),\n ).rejects.toThrow(GraffitiErrorUnauthorized);\n });\n\n test(\"export\", async () => {\n // Put a whole bunch of stuff so the export needs to page\n const keys = new Set<string>();\n for (let i = 0; i < 256; i++) {\n const key = Math.random().toString(36).substring(2, 15);\n keys.add(key);\n\n const input = \"Hello world \" + i;\n const bytes = new TextEncoder().encode(input);\n await storageBuckets.put(\n storageBucketEndpoint,\n key,\n bytes,\n storageBucketToken,\n );\n }\n\n // Export\n const retrievedKeys = new Set<string>();\n const iterator = storageBuckets.export(\n storageBucketEndpoint,\n storageBucketToken,\n );\n for await (const result of iterator) {\n if (keys.has(result.key)) {\n retrievedKeys.add(result.key);\n }\n }\n expect(retrievedKeys.size).toEqual(keys.size);\n\n // Delete all the keys\n for (const key of keys) {\n await storageBuckets.delete(\n storageBucketEndpoint,\n key,\n storageBucketToken,\n );\n }\n }, 1000000);\n });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["dagCborEncode", "dagCborDecode"]
|
|
7
7
|
}
|
|
@@ -24,8 +24,17 @@ module.exports = __toCommonJS(storage_buckets_exports);
|
|
|
24
24
|
var import_utilities = require("./utilities");
|
|
25
25
|
var import_mini = require("zod/mini");
|
|
26
26
|
var import_dag_cbor = require("@ipld/dag-cbor");
|
|
27
|
+
var import_api = require("@graffiti-garden/api");
|
|
28
|
+
const BATCH_KEYS = 32;
|
|
29
|
+
const BATCH_TOTAL_VALUE_BYTES = 1024 * 1024;
|
|
30
|
+
const BATCH_WAIT_MS = 2;
|
|
31
|
+
function batchWidth(maxBytes) {
|
|
32
|
+
return Math.min(BATCH_KEYS, Math.floor(BATCH_TOTAL_VALUE_BYTES / maxBytes));
|
|
33
|
+
}
|
|
27
34
|
class StorageBuckets {
|
|
28
35
|
getAuthorizationEndpoint = import_utilities.getAuthorizationEndpoint;
|
|
36
|
+
queues = /* @__PURE__ */ new Map();
|
|
37
|
+
unsupportedBatchEndpoints = /* @__PURE__ */ new Set();
|
|
29
38
|
async put(storageBucketEndpoint, key, value, authorizationToken) {
|
|
30
39
|
(0, import_utilities.verifyHTTPSEndpoint)(storageBucketEndpoint);
|
|
31
40
|
const url = `${storageBucketEndpoint}/value/${encodeURIComponent(key)}`;
|
|
@@ -50,6 +59,88 @@ class StorageBuckets {
|
|
|
50
59
|
}
|
|
51
60
|
async get(storageBucketEndpoint, key, maxBytes) {
|
|
52
61
|
(0, import_utilities.verifyHTTPSEndpoint)(storageBucketEndpoint);
|
|
62
|
+
if (maxBytes !== void 0 && Number.isSafeInteger(maxBytes) && maxBytes > 0 && batchWidth(maxBytes) >= 2 && !this.unsupportedBatchEndpoints.has(storageBucketEndpoint)) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
let queuesByMaxBytes = this.queues.get(storageBucketEndpoint);
|
|
65
|
+
if (!queuesByMaxBytes) {
|
|
66
|
+
queuesByMaxBytes = /* @__PURE__ */ new Map();
|
|
67
|
+
this.queues.set(storageBucketEndpoint, queuesByMaxBytes);
|
|
68
|
+
}
|
|
69
|
+
let queue = queuesByMaxBytes.get(maxBytes);
|
|
70
|
+
if (!queue) {
|
|
71
|
+
queue = { requests: [] };
|
|
72
|
+
queuesByMaxBytes.set(maxBytes, queue);
|
|
73
|
+
}
|
|
74
|
+
queue.requests.push({ key, resolve, reject });
|
|
75
|
+
if (queue.requests.length === batchWidth(maxBytes)) {
|
|
76
|
+
this.flush(storageBucketEndpoint, maxBytes, queue);
|
|
77
|
+
} else if (queue.timer === void 0) {
|
|
78
|
+
queue.timer = setTimeout(
|
|
79
|
+
() => this.flush(storageBucketEndpoint, maxBytes, queue),
|
|
80
|
+
BATCH_WAIT_MS
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return this.getOne(storageBucketEndpoint, key, maxBytes);
|
|
86
|
+
}
|
|
87
|
+
flush(endpoint, maxBytes, queue) {
|
|
88
|
+
if (queue.timer !== void 0) clearTimeout(queue.timer);
|
|
89
|
+
const requests = queue.requests;
|
|
90
|
+
const queuesByMaxBytes = this.queues.get(endpoint);
|
|
91
|
+
queuesByMaxBytes.delete(maxBytes);
|
|
92
|
+
if (queuesByMaxBytes.size === 0) this.queues.delete(endpoint);
|
|
93
|
+
if (requests.length === 1 || this.unsupportedBatchEndpoints.has(endpoint)) {
|
|
94
|
+
requests.forEach(({ key, resolve, reject }) => {
|
|
95
|
+
void this.getOne(endpoint, key, maxBytes).then(resolve, reject);
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
void this.getBatch(endpoint, maxBytes, requests);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async getBatch(endpoint, maxBytes, requests) {
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch(`${endpoint}/values`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "Content-Type": "application/cbor", Accept: "application/cbor" },
|
|
106
|
+
body: (0, import_dag_cbor.encode)({
|
|
107
|
+
keys: requests.map(({ key }) => key),
|
|
108
|
+
maxValueBytes: maxBytes
|
|
109
|
+
}).slice()
|
|
110
|
+
});
|
|
111
|
+
if (response.status === 404) {
|
|
112
|
+
this.unsupportedBatchEndpoints.add(endpoint);
|
|
113
|
+
await Promise.all(requests.map(async ({ key, resolve, reject }) => {
|
|
114
|
+
try {
|
|
115
|
+
resolve(await this.getOne(endpoint, key, maxBytes));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
reject(error);
|
|
118
|
+
}
|
|
119
|
+
}));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!response.ok) throw new Error(await response.text());
|
|
123
|
+
const decoded = (0, import_dag_cbor.decode)(await response.arrayBuffer());
|
|
124
|
+
if (!decoded || typeof decoded !== "object" || !("results" in decoded) || !Array.isArray(decoded.results) || decoded.results.length !== requests.length) throw new Error("Invalid storage batch response");
|
|
125
|
+
decoded.results.forEach((result, index) => {
|
|
126
|
+
const { resolve, reject } = requests[index];
|
|
127
|
+
if (!result || typeof result !== "object" || !("status" in result)) {
|
|
128
|
+
reject(new Error("Invalid storage batch result"));
|
|
129
|
+
} else if (result.status === 200 && "value" in result && result.value instanceof Uint8Array && result.value.byteLength <= maxBytes) {
|
|
130
|
+
resolve(result.value);
|
|
131
|
+
} else if (result.status === 404) {
|
|
132
|
+
reject(new import_api.GraffitiErrorNotFound("Value not found"));
|
|
133
|
+
} else if (result.status === 413) {
|
|
134
|
+
reject(new import_api.GraffitiErrorTooLarge("Value exceeds maximum byte limit"));
|
|
135
|
+
} else {
|
|
136
|
+
reject(new Error("Invalid storage batch result"));
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
} catch (error) {
|
|
140
|
+
requests.forEach(({ reject }) => reject(error));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async getOne(storageBucketEndpoint, key, maxBytes) {
|
|
53
144
|
const url = `${storageBucketEndpoint}/value/${encodeURIComponent(key)}`;
|
|
54
145
|
const response = await (0, import_utilities.fetchWithErrorHandling)(url);
|
|
55
146
|
const reader = response.body?.getReader();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/1-services/3-storage-buckets.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n fetchWithErrorHandling,\n getAuthorizationEndpoint,\n verifyHTTPSEndpoint,\n} from \"./utilities\";\nimport { string, array, object, optional, nullable } from \"zod/mini\";\nimport { decode as dagCborDecode } from \"@ipld/dag-cbor\";\n\nexport class StorageBuckets {\n getAuthorizationEndpoint = getAuthorizationEndpoint;\n\n async put(\n storageBucketEndpoint: string,\n key: string,\n value: Uint8Array,\n authorizationToken: string,\n ): Promise<void> {\n verifyHTTPSEndpoint(storageBucketEndpoint);\n const url = `${storageBucketEndpoint}/value/${encodeURIComponent(key)}`;\n\n await fetchWithErrorHandling(url, {\n method: \"PUT\",\n headers: {\n \"Content-Type\": \"application/octet-stream\",\n Authorization: `Bearer ${authorizationToken}`,\n },\n body: value.slice(),\n });\n }\n\n async delete(\n storageBucketEndpoint: string,\n key: string,\n authorizationToken: string,\n ): Promise<void> {\n verifyHTTPSEndpoint(storageBucketEndpoint);\n const url = `${storageBucketEndpoint}/value/${encodeURIComponent(key)}`;\n\n await fetchWithErrorHandling(url, {\n method: \"DELETE\",\n headers: {\n Authorization: `Bearer ${authorizationToken}`,\n },\n });\n }\n\n async get(\n storageBucketEndpoint: string,\n key: string,\n maxBytes?: number,\n ): Promise<Uint8Array> {\n verifyHTTPSEndpoint(storageBucketEndpoint);\n const url = `${storageBucketEndpoint}/value/${encodeURIComponent(key)}`;\n\n const response = await fetchWithErrorHandling(url);\n\n const reader = response.body?.getReader();\n if (!reader) {\n throw new Error(\"Failed to read value from storage bucket\");\n }\n\n const contentLengthHeader = response.headers.get(\"Content-Length\");\n const parsedContentLength = contentLengthHeader\n ? Number(contentLengthHeader)\n : undefined;\n\n const hasValidContentLength =\n !!parsedContentLength &&\n !!Number.isFinite(parsedContentLength) &&\n parsedContentLength >= 0;\n\n // Fast path: Content-Length exists and is valid\n if (hasValidContentLength) {\n const contentLength = parsedContentLength!;\n if (maxBytes !== undefined && contentLength > maxBytes) {\n throw new Error(\"Value exceeds maximum byte limit\");\n }\n\n const out = new Uint8Array(contentLength);\n let offset = 0;\n let completed = false;\n\n try {\n while (offset <= out.length) {\n const { done, value } = await reader.read();\n\n if (done) {\n completed = true;\n break;\n }\n if (!value || value.length === 0) continue;\n\n const nextOffset = offset + value.length;\n if (nextOffset > out.length) {\n throw new Error(\"Received more data than expected\");\n }\n\n out.set(value, offset);\n offset = nextOffset;\n }\n } finally {\n reader.releaseLock();\n }\n\n if (!completed) {\n throw new Error(\"Failed to read complete value from storage bucket\");\n }\n\n return offset === contentLength ? out : out.slice(0, offset);\n }\n\n // Fallback path: no (usable) Content-Length\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) break;\n if (!value || value.length === 0) continue;\n\n total += value.length;\n if (maxBytes !== undefined && total > maxBytes) {\n throw new Error(\"Value exceeds maximum byte limit\");\n }\n\n // Copy because some implementations reuse the underlying buffer\n chunks.push(value.slice());\n }\n } finally {\n reader.releaseLock();\n }\n\n // Concatenate chunks into one Uint8Array\n const out = new Uint8Array(total);\n let offset = 0;\n for (const c of chunks) {\n out.set(c, offset);\n offset += c.length;\n }\n return out;\n }\n\n async *export(\n storageBucketEndpoint: string,\n authorizationToken: string,\n ): AsyncGenerator<{ key: string }> {\n verifyHTTPSEndpoint(storageBucketEndpoint);\n const url = `${storageBucketEndpoint}/export`;\n\n let cursor: string | undefined = undefined;\n while (true) {\n const response = await fetchWithErrorHandling(\n cursor ? `${url}?cursor=${encodeURIComponent(cursor)}` : url,\n {\n headers: {\n Authorization: `Bearer ${authorizationToken}`,\n },\n },\n );\n\n const blob = await response.blob();\n const cbor = dagCborDecode(await blob.arrayBuffer());\n const data = ExportSchema.parse(cbor);\n\n for (const key of data.keys) {\n yield { key };\n }\n\n if (data.cursor) {\n cursor = data.cursor;\n } else {\n break;\n }\n }\n }\n}\n\nconst ExportSchema = object({\n keys: array(string()),\n cursor: optional(nullable(string())),\n});\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAIO;AACP,kBAA0D;AAC1D,
|
|
6
|
-
"names": ["
|
|
4
|
+
"sourcesContent": ["import {\n fetchWithErrorHandling,\n getAuthorizationEndpoint,\n verifyHTTPSEndpoint,\n} from \"./utilities\";\nimport { string, array, object, optional, nullable } from \"zod/mini\";\nimport { encode as dagCborEncode, decode as dagCborDecode } from \"@ipld/dag-cbor\";\nimport { GraffitiErrorNotFound, GraffitiErrorTooLarge } from \"@graffiti-garden/api\";\n\n// All batch-capable bucket servers accept these transport limits.\nconst BATCH_KEYS = 32; // total number of keys in a batch request\nconst BATCH_TOTAL_VALUE_BYTES = 1024 * 1024; // total response limit\n\n// This is a heuristic for how long to wait to fill a batch request\n// before sending it. Just enough waiting to capture a few more requests,\n// but not so long that it adds noticeable latency.\nconst BATCH_WAIT_MS = 2;\n\nfunction batchWidth(maxBytes: number) {\n return Math.min(BATCH_KEYS, Math.floor(BATCH_TOTAL_VALUE_BYTES / maxBytes));\n}\n\ntype PendingGet = {\n key: string;\n resolve: (value: Uint8Array) => void;\n reject: (reason: unknown) => void;\n};\ntype BatchQueue = { requests: PendingGet[]; timer?: ReturnType<typeof setTimeout> };\n\nexport class StorageBuckets {\n getAuthorizationEndpoint = getAuthorizationEndpoint;\n private readonly queues = new Map<string, Map<number, BatchQueue>>();\n private readonly unsupportedBatchEndpoints = new Set<string>();\n\n async put(\n storageBucketEndpoint: string,\n key: string,\n value: Uint8Array,\n authorizationToken: string,\n ): Promise<void> {\n verifyHTTPSEndpoint(storageBucketEndpoint);\n const url = `${storageBucketEndpoint}/value/${encodeURIComponent(key)}`;\n\n await fetchWithErrorHandling(url, {\n method: \"PUT\",\n headers: {\n \"Content-Type\": \"application/octet-stream\",\n Authorization: `Bearer ${authorizationToken}`,\n },\n body: value.slice(),\n });\n }\n\n async delete(\n storageBucketEndpoint: string,\n key: string,\n authorizationToken: string,\n ): Promise<void> {\n verifyHTTPSEndpoint(storageBucketEndpoint);\n const url = `${storageBucketEndpoint}/value/${encodeURIComponent(key)}`;\n\n await fetchWithErrorHandling(url, {\n method: \"DELETE\",\n headers: {\n Authorization: `Bearer ${authorizationToken}`,\n },\n });\n }\n\n async get(\n storageBucketEndpoint: string,\n key: string,\n maxBytes?: number,\n ): Promise<Uint8Array> {\n verifyHTTPSEndpoint(storageBucketEndpoint);\n if (\n maxBytes !== undefined &&\n Number.isSafeInteger(maxBytes) &&\n maxBytes > 0 &&\n batchWidth(maxBytes) >= 2 &&\n !this.unsupportedBatchEndpoints.has(storageBucketEndpoint)\n ) {\n return new Promise<Uint8Array>((resolve, reject) => {\n let queuesByMaxBytes = this.queues.get(storageBucketEndpoint);\n if (!queuesByMaxBytes) {\n queuesByMaxBytes = new Map();\n this.queues.set(storageBucketEndpoint, queuesByMaxBytes);\n }\n let queue = queuesByMaxBytes.get(maxBytes);\n if (!queue) {\n queue = { requests: [] };\n queuesByMaxBytes.set(maxBytes, queue);\n }\n queue.requests.push({ key, resolve, reject });\n if (queue.requests.length === batchWidth(maxBytes)) {\n this.flush(storageBucketEndpoint, maxBytes, queue);\n } else if (queue.timer === undefined) {\n queue.timer = setTimeout(\n () => this.flush(storageBucketEndpoint, maxBytes, queue),\n BATCH_WAIT_MS,\n );\n }\n });\n }\n return this.getOne(storageBucketEndpoint, key, maxBytes);\n }\n\n private flush(endpoint: string, maxBytes: number, queue: BatchQueue) {\n if (queue.timer !== undefined) clearTimeout(queue.timer);\n const requests = queue.requests;\n const queuesByMaxBytes = this.queues.get(endpoint)!;\n queuesByMaxBytes.delete(maxBytes);\n if (queuesByMaxBytes.size === 0) this.queues.delete(endpoint);\n if (requests.length === 1 || this.unsupportedBatchEndpoints.has(endpoint)) {\n requests.forEach(({ key, resolve, reject }) => {\n void this.getOne(endpoint, key, maxBytes).then(resolve, reject);\n });\n } else {\n void this.getBatch(endpoint, maxBytes, requests);\n }\n }\n\n private async getBatch(endpoint: string, maxBytes: number, requests: PendingGet[]) {\n try {\n const response = await fetch(`${endpoint}/values`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/cbor\", Accept: \"application/cbor\" },\n body: dagCborEncode({\n keys: requests.map(({ key }) => key),\n maxValueBytes: maxBytes,\n }).slice(),\n });\n if (response.status === 404) {\n // An older bucket server has no /values route but still serves GETs.\n this.unsupportedBatchEndpoints.add(endpoint);\n await Promise.all(requests.map(async ({ key, resolve, reject }) => {\n try {\n resolve(await this.getOne(endpoint, key, maxBytes));\n } catch (error) {\n reject(error);\n }\n }));\n return;\n }\n if (!response.ok) throw new Error(await response.text());\n const decoded = dagCborDecode(await response.arrayBuffer());\n if (\n !decoded || typeof decoded !== \"object\" ||\n !(\"results\" in decoded) || !Array.isArray(decoded.results) ||\n decoded.results.length !== requests.length\n ) throw new Error(\"Invalid storage batch response\");\n\n decoded.results.forEach((result: unknown, index: number) => {\n const { resolve, reject } = requests[index];\n if (!result || typeof result !== \"object\" || !(\"status\" in result)) {\n reject(new Error(\"Invalid storage batch result\"));\n } else if (\n result.status === 200 && \"value\" in result &&\n result.value instanceof Uint8Array &&\n result.value.byteLength <= maxBytes\n ) {\n resolve(result.value);\n } else if (result.status === 404) {\n reject(new GraffitiErrorNotFound(\"Value not found\"));\n } else if (result.status === 413) {\n reject(new GraffitiErrorTooLarge(\"Value exceeds maximum byte limit\"));\n } else {\n reject(new Error(\"Invalid storage batch result\"));\n }\n });\n } catch (error) {\n requests.forEach(({ reject }) => reject(error));\n }\n }\n\n private async getOne(\n storageBucketEndpoint: string,\n key: string,\n maxBytes?: number,\n ): Promise<Uint8Array> {\n const url = `${storageBucketEndpoint}/value/${encodeURIComponent(key)}`;\n\n const response = await fetchWithErrorHandling(url);\n\n const reader = response.body?.getReader();\n if (!reader) {\n throw new Error(\"Failed to read value from storage bucket\");\n }\n\n const contentLengthHeader = response.headers.get(\"Content-Length\");\n const parsedContentLength = contentLengthHeader\n ? Number(contentLengthHeader)\n : undefined;\n\n const hasValidContentLength =\n !!parsedContentLength &&\n !!Number.isFinite(parsedContentLength) &&\n parsedContentLength >= 0;\n\n // Fast path: Content-Length exists and is valid\n if (hasValidContentLength) {\n const contentLength = parsedContentLength!;\n if (maxBytes !== undefined && contentLength > maxBytes) {\n throw new Error(\"Value exceeds maximum byte limit\");\n }\n\n const out = new Uint8Array(contentLength);\n let offset = 0;\n let completed = false;\n\n try {\n while (offset <= out.length) {\n const { done, value } = await reader.read();\n\n if (done) {\n completed = true;\n break;\n }\n if (!value || value.length === 0) continue;\n\n const nextOffset = offset + value.length;\n if (nextOffset > out.length) {\n throw new Error(\"Received more data than expected\");\n }\n\n out.set(value, offset);\n offset = nextOffset;\n }\n } finally {\n reader.releaseLock();\n }\n\n if (!completed) {\n throw new Error(\"Failed to read complete value from storage bucket\");\n }\n\n return offset === contentLength ? out : out.slice(0, offset);\n }\n\n // Fallback path: no (usable) Content-Length\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) break;\n if (!value || value.length === 0) continue;\n\n total += value.length;\n if (maxBytes !== undefined && total > maxBytes) {\n throw new Error(\"Value exceeds maximum byte limit\");\n }\n\n // Copy because some implementations reuse the underlying buffer\n chunks.push(value.slice());\n }\n } finally {\n reader.releaseLock();\n }\n\n // Concatenate chunks into one Uint8Array\n const out = new Uint8Array(total);\n let offset = 0;\n for (const c of chunks) {\n out.set(c, offset);\n offset += c.length;\n }\n return out;\n }\n\n async *export(\n storageBucketEndpoint: string,\n authorizationToken: string,\n ): AsyncGenerator<{ key: string }> {\n verifyHTTPSEndpoint(storageBucketEndpoint);\n const url = `${storageBucketEndpoint}/export`;\n\n let cursor: string | undefined = undefined;\n while (true) {\n const response = await fetchWithErrorHandling(\n cursor ? `${url}?cursor=${encodeURIComponent(cursor)}` : url,\n {\n headers: {\n Authorization: `Bearer ${authorizationToken}`,\n },\n },\n );\n\n const blob = await response.blob();\n const cbor = dagCborDecode(await blob.arrayBuffer());\n const data = ExportSchema.parse(cbor);\n\n for (const key of data.keys) {\n yield { key };\n }\n\n if (data.cursor) {\n cursor = data.cursor;\n } else {\n break;\n }\n }\n }\n}\n\nconst ExportSchema = object({\n keys: array(string()),\n cursor: optional(nullable(string())),\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["dagCborEncode", "dagCborDecode", "out", "offset"]
|
|
7
7
|
}
|
|
@@ -25,6 +25,7 @@ var import_vitest = require("vitest");
|
|
|
25
25
|
var import_inboxes = require("./4-inboxes");
|
|
26
26
|
var import_utilities = require("./utilities");
|
|
27
27
|
var import_utils = require("@noble/hashes/utils.js");
|
|
28
|
+
var import_dag_cbor = require("@ipld/dag-cbor");
|
|
28
29
|
function inboxTests(inboxEndpoint, inboxToken) {
|
|
29
30
|
(0, import_vitest.describe)("Inboxes", async () => {
|
|
30
31
|
const inboxes = new import_inboxes.Inboxes();
|
|
@@ -130,6 +131,25 @@ function inboxTests(inboxEndpoint, inboxToken) {
|
|
|
130
131
|
}
|
|
131
132
|
});
|
|
132
133
|
}
|
|
134
|
+
let cursor;
|
|
135
|
+
for (const [page, expectedCount] of [10, 100, 101].entries()) {
|
|
136
|
+
const response = await fetch(
|
|
137
|
+
`${inboxEndpoint}/query${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`,
|
|
138
|
+
{
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/cbor",
|
|
142
|
+
Authorization: `Bearer ${inboxToken}`
|
|
143
|
+
},
|
|
144
|
+
body: cursor ? void 0 : (0, import_dag_cbor.encode)({ tags: [tags[1]], schema: {} }).slice()
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
(0, import_vitest.expect)(response.status).toBe(200);
|
|
148
|
+
const pageResult = (0, import_dag_cbor.decode)(new Uint8Array(await response.arrayBuffer()));
|
|
149
|
+
(0, import_vitest.expect)(pageResult.results).toHaveLength(expectedCount);
|
|
150
|
+
(0, import_vitest.expect)(pageResult.hasMore).toBe(page < 2);
|
|
151
|
+
cursor = pageResult.cursor;
|
|
152
|
+
}
|
|
133
153
|
const iterator = inboxes.query(
|
|
134
154
|
inboxEndpoint,
|
|
135
155
|
[(0, import_utils.randomBytes)(), tags[1], (0, import_utils.randomBytes)()],
|