From 02228b9c7615f254845d64b6eb7327819729de43 Mon Sep 17 00:00:00 2001 From: Cesare Naldi Date: Tue, 19 Aug 2025 14:39:55 +0200 Subject: [PATCH 1/4] chore: upgrades biome@2.x --- biome.json | 22 +++++--- package.json | 22 ++++++-- pnpm-lock.yaml | 76 +++++++++++++-------------- src/StorageClient.test.ts | 51 ++++++++++++------ src/StorageClient.ts | 105 ++++++++++++++++++++++++++++---------- src/builders.ts | 10 +++- src/errors.ts | 2 +- src/types.ts | 12 +++-- src/utils.ts | 39 ++++++++++---- 9 files changed, 233 insertions(+), 106 deletions(-) diff --git a/biome.json b/biome.json index 0933042..55911ef 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json", "vcs": { "enabled": true, "clientKind": "git", @@ -7,19 +7,29 @@ }, "files": { "ignoreUnknown": false, - "ignore": [] + "includes": ["**"] }, "formatter": { "enabled": true, "useEditorconfig": true }, - "organizeImports": { - "enabled": true - }, + "assist": { "actions": { "source": { "organizeImports": "on" } } }, "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "style": { + "noParameterAssign": "error", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error", + "noUnusedTemplateLiteral": "error", + "useNumberNamespace": "error", + "noInferrableTypes": "error", + "noUselessElse": "error" + } } }, "javascript": { diff --git a/package.json b/package.json index 2b42ffe..0edbda9 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,16 @@ "module": "dist/index.js", "types": "dist/index.d.ts", "sideEffects": false, - "files": ["dist"], - "keywords": ["lens", "grove", "chain", "storage", "node"], + "files": [ + "dist" + ], + "keywords": [ + "lens", + "grove", + "chain", + "storage", + "node" + ], "exports": { ".": { "import": "./dist/index.js", @@ -17,8 +25,12 @@ }, "typesVersions": { "*": { - "import": ["./dist/index.d.ts"], - "require": ["./dist/index.d.ts"] + "import": [ + "./dist/index.d.ts" + ], + "require": [ + "./dist/index.d.ts" + ] } }, "scripts": { @@ -30,7 +42,7 @@ "test:browser": "vitest --project browser" }, "devDependencies": { - "@biomejs/biome": "^1.9.4", + "@biomejs/biome": "^2.0.6", "@changesets/cli": "^2.27.9", "@types/node": "^22.9.0", "@vitest/browser": "^3.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f4e231..10683aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: devDependencies: '@biomejs/biome': - specifier: ^1.9.4 - version: 1.9.4 + specifier: ^2.0.6 + version: 2.0.6 '@changesets/cli': specifier: ^2.27.9 version: 2.27.9 @@ -62,55 +62,55 @@ packages: resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} engines: {node: '>=6.9.0'} - '@biomejs/biome@1.9.4': - resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + '@biomejs/biome@2.0.6': + resolution: {integrity: sha512-RRP+9cdh5qwe2t0gORwXaa27oTOiQRQvrFf49x2PA1tnpsyU7FIHX4ZOFMtBC4QNtyWsN7Dqkf5EDbg4X+9iqA==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@1.9.4': - resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + '@biomejs/cli-darwin-arm64@2.0.6': + resolution: {integrity: sha512-AzdiNNjNzsE6LfqWyBvcL29uWoIuZUkndu+wwlXW13EKcBHbbKjNQEZIJKYDc6IL+p7bmWGx3v9ZtcRyIoIz5A==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@1.9.4': - resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + '@biomejs/cli-darwin-x64@2.0.6': + resolution: {integrity: sha512-wJjjP4E7bO4WJmiQaLnsdXMa516dbtC6542qeRkyJg0MqMXP0fvs4gdsHhZ7p9XWTAmGIjZHFKXdsjBvKGIJJQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@1.9.4': - resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + '@biomejs/cli-linux-arm64-musl@2.0.6': + resolution: {integrity: sha512-CVPEMlin3bW49sBqLBg2x016Pws7eUXA27XYDFlEtponD0luYjg2zQaMJ2nOqlkKG9fqzzkamdYxHdMDc2gZFw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-arm64@1.9.4': - resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + '@biomejs/cli-linux-arm64@2.0.6': + resolution: {integrity: sha512-ZSVf6TYo5rNMUHIW1tww+rs/krol7U5A1Is/yzWyHVZguuB0lBnIodqyFuwCNqG9aJGyk7xIMS8HG0qGUPz0SA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-x64-musl@1.9.4': - resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + '@biomejs/cli-linux-x64-musl@2.0.6': + resolution: {integrity: sha512-mKHE/e954hR/hSnAcJSjkf4xGqZc/53Kh39HVW1EgO5iFi0JutTN07TSjEMg616julRtfSNJi0KNyxvc30Y4rQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-linux-x64@1.9.4': - resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + '@biomejs/cli-linux-x64@2.0.6': + resolution: {integrity: sha512-geM1MkHTV1Kh2Cs/Xzot9BOF3WBacihw6bkEmxkz4nSga8B9/hWy5BDiOG3gHDGIBa8WxT0nzsJs2f/hPqQIQw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-win32-arm64@1.9.4': - resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + '@biomejs/cli-win32-arm64@2.0.6': + resolution: {integrity: sha512-290V4oSFoKaprKE1zkYVsDfAdn0An5DowZ+GIABgjoq1ndhvNxkJcpxPsiYtT7slbVe3xmlT0ncdfOsN7KruzA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@1.9.4': - resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + '@biomejs/cli-win32-x64@2.0.6': + resolution: {integrity: sha512-bfM1Bce0d69Ao7pjTjUS+AWSZ02+5UHdiAP85Th8e9yV5xzw6JrHXbL5YWlcEKQ84FIZMdDc7ncuti1wd2sdbw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -2598,39 +2598,39 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@biomejs/biome@1.9.4': + '@biomejs/biome@2.0.6': optionalDependencies: - '@biomejs/cli-darwin-arm64': 1.9.4 - '@biomejs/cli-darwin-x64': 1.9.4 - '@biomejs/cli-linux-arm64': 1.9.4 - '@biomejs/cli-linux-arm64-musl': 1.9.4 - '@biomejs/cli-linux-x64': 1.9.4 - '@biomejs/cli-linux-x64-musl': 1.9.4 - '@biomejs/cli-win32-arm64': 1.9.4 - '@biomejs/cli-win32-x64': 1.9.4 - - '@biomejs/cli-darwin-arm64@1.9.4': + '@biomejs/cli-darwin-arm64': 2.0.6 + '@biomejs/cli-darwin-x64': 2.0.6 + '@biomejs/cli-linux-arm64': 2.0.6 + '@biomejs/cli-linux-arm64-musl': 2.0.6 + '@biomejs/cli-linux-x64': 2.0.6 + '@biomejs/cli-linux-x64-musl': 2.0.6 + '@biomejs/cli-win32-arm64': 2.0.6 + '@biomejs/cli-win32-x64': 2.0.6 + + '@biomejs/cli-darwin-arm64@2.0.6': optional: true - '@biomejs/cli-darwin-x64@1.9.4': + '@biomejs/cli-darwin-x64@2.0.6': optional: true - '@biomejs/cli-linux-arm64-musl@1.9.4': + '@biomejs/cli-linux-arm64-musl@2.0.6': optional: true - '@biomejs/cli-linux-arm64@1.9.4': + '@biomejs/cli-linux-arm64@2.0.6': optional: true - '@biomejs/cli-linux-x64-musl@1.9.4': + '@biomejs/cli-linux-x64-musl@2.0.6': optional: true - '@biomejs/cli-linux-x64@1.9.4': + '@biomejs/cli-linux-x64@2.0.6': optional: true - '@biomejs/cli-win32-arm64@1.9.4': + '@biomejs/cli-win32-arm64@2.0.6': optional: true - '@biomejs/cli-win32-x64@1.9.4': + '@biomejs/cli-win32-x64@2.0.6': optional: true '@changesets/apply-release-plan@7.0.5': diff --git a/src/StorageClient.test.ts b/src/StorageClient.test.ts index 99fbc04..47923d8 100644 --- a/src/StorageClient.test.ts +++ b/src/StorageClient.test.ts @@ -1,9 +1,8 @@ import { privateKeyToAccount } from 'viem/accounts'; import { describe, expect, it } from 'vitest'; - -import { StorageClient } from './StorageClient'; import { immutable, lensAccountOnly, walletOnly } from './builders'; import { staging } from './environments'; +import { StorageClient } from './StorageClient'; import { FileUploadResponse, type Resource } from './types'; import { never } from './utils'; @@ -73,7 +72,9 @@ describe(`Given an instance of the '${StorageClient.name}'`, () => { const files = [file1, file2]; it('Then it should create the expected resources', async () => { - const result = await client.uploadFolder(files, { acl: immutable(37111) }); + const result = await client.uploadFolder(files, { + acl: immutable(37111), + }); await assertFileExist(client.resolve(result.files[0]?.uri ?? never())); }); @@ -88,14 +89,21 @@ describe(`Given an instance of the '${StorageClient.name}'`, () => { const res = await fetch(url); const json = await res.json(); expect(json).toMatchObject({ - files: expect.arrayContaining([expect.any(String), expect.any(String), expect.any(String)]), + files: expect.arrayContaining([ + expect.any(String), + expect.any(String), + expect.any(String), + ]), }); }); it('Then it should support using an arbitrary index file', async () => { const index = new File(['[]'], 'index.json', { type: 'text/plain' }); - const response = await client.uploadFolder(files, { index, acl: immutable(37111) }); + const response = await client.uploadFolder(files, { + index, + acl: immutable(37111), + }); const url = client.resolve(response.folder.uri); const res = await fetch(url); @@ -104,7 +112,8 @@ describe(`Given an instance of the '${StorageClient.name}'`, () => { }); it('Then it should support an index file factory', async () => { - const indexFactory = (resources: Resource[]) => resources.map((r) => r.uri); + const indexFactory = (resources: Resource[]) => + resources.map((r) => r.uri); const response = await client.uploadFolder(files, { index: indexFactory, acl: immutable(37111), @@ -113,7 +122,9 @@ describe(`Given an instance of the '${StorageClient.name}'`, () => { const url = client.resolve(response.folder.uri); const res = await fetch(url); const json = await res.json(); - expect(json).toMatchObject(expect.arrayContaining([expect.stringContaining('lens://')])); + expect(json).toMatchObject( + expect.arrayContaining([expect.stringContaining('lens://')]), + ); }); it('Then it should allow to specify you own index file', async () => { @@ -121,7 +132,10 @@ describe(`Given an instance of the '${StorageClient.name}'`, () => { const index = new File([JSON.stringify(content)], 'index.json', { type: 'application/json', }); - const response = await client.uploadFolder(files, { index, acl: immutable(37111) }); + const response = await client.uploadFolder(files, { + index, + acl: immutable(37111), + }); const url = client.resolve(response.folder.uri); const res = await fetch(url); @@ -139,9 +153,9 @@ describe(`Given an instance of the '${StorageClient.name}'`, () => { const response = await client.uploadFile(file1, { acl }); await response.waitForPropagation(); - await expect(client.editFile(response.uri, file2, signer, { acl })).resolves.toBeInstanceOf( - FileUploadResponse, - ); + await expect( + client.editFile(response.uri, file2, signer, { acl }), + ).resolves.toBeInstanceOf(FileUploadResponse); }, ); }); @@ -155,7 +169,9 @@ describe(`Given an instance of the '${StorageClient.name}'`, () => { const response = await client.uploadFile(file1, { acl }); await response.waitForPropagation(); - await expect(client.delete(response.uri, signer)).resolves.toHaveProperty('success', true); + await expect( + client.delete(response.uri, signer), + ).resolves.toHaveProperty('success', true); }, ); }); @@ -169,9 +185,9 @@ describe(`Given an instance of the '${StorageClient.name}'`, () => { const response = await client.uploadFile(file1, { acl }); await response.waitForPropagation(); - await expect(client.editFile(response.uri, file2, signer, { acl })).resolves.toBeInstanceOf( - FileUploadResponse, - ); + await expect( + client.editFile(response.uri, file2, signer, { acl }), + ).resolves.toBeInstanceOf(FileUploadResponse); }, ); }); @@ -182,7 +198,10 @@ describe(`Given an instance of the '${StorageClient.name}'`, () => { const response = await client.uploadFile(file1, { acl }); await response.waitForPropagation(); - await expect(client.delete(response.uri, signer)).resolves.toHaveProperty('success', true); + await expect(client.delete(response.uri, signer)).resolves.toHaveProperty( + 'success', + true, + ); }); }); }); diff --git a/src/StorageClient.ts b/src/StorageClient.ts index 42af3a6..8558ec9 100644 --- a/src/StorageClient.ts +++ b/src/StorageClient.ts @@ -1,4 +1,7 @@ -import { type Authorization, AuthorizationService } from './AuthorizationService'; +import { + type Authorization, + AuthorizationService, +} from './AuthorizationService'; import { immutable } from './builders'; import { type EnvironmentConfig, production } from './environments'; import { AuthorizationError, StorageClientError } from './errors'; @@ -17,11 +20,11 @@ import { type UploadJsonOptions, } from './types'; import { - MultipartEntriesBuilder, - type MultipartEntry, createMultipartRequestInit, extractStorageKey, invariant, + MultipartEntriesBuilder, + type MultipartEntry, never, resourceFrom, statusFrom, @@ -52,12 +55,18 @@ export class StorageClient { * @param options - Any additional options for the upload * @returns The {@link FileUploadResponse} to the uploaded file */ - async uploadFile(file: File, options: UploadFileOptions): Promise; + async uploadFile( + file: File, + options: UploadFileOptions, + ): Promise; /** * * @deprecated use `uploadFile(file: File, options: UploadFileOptions): Promise` instead */ - async uploadFile(file: File, options?: UploadFileOptions): Promise; + async uploadFile( + file: File, + options?: UploadFileOptions, + ): Promise; async uploadFile( file: File, { acl }: UploadFileOptions = { acl: immutable(this.env.defaultChainId) }, @@ -85,11 +94,17 @@ export class StorageClient { * @param options - Upload options including the ACL configuration * @returns The {@link FileUploadResponse} to the uploaded JSON */ - async uploadAsJson(json: unknown, options: UploadJsonOptions): Promise; + async uploadAsJson( + json: unknown, + options: UploadJsonOptions, + ): Promise; /** * @deprecated use `uploadAsJson(json: unknown, options: UploadJsonOptions): Promise` instead */ - async uploadAsJson(json: unknown, options?: UploadJsonOptions): Promise; + async uploadAsJson( + json: unknown, + options?: UploadJsonOptions, + ): Promise; async uploadAsJson( json: unknown, options: UploadJsonOptions = { acl: immutable(this.env.defaultChainId) }, @@ -128,7 +143,9 @@ export class StorageClient { files.length + (needsIndex ? 2 : 1), ); - const builder = MultipartEntriesBuilder.from(fileResources).withFiles(Array.from(files)); + const builder = MultipartEntriesBuilder.from(fileResources).withFiles( + Array.from(files), + ); if (options.index) { builder.withIndexFile(options.index); @@ -137,7 +154,7 @@ export class StorageClient { builder.withAclTemplate(options.acl); const entries = builder.build(); - const response = await this.create(folderResource.storageKey, entries); + const response = await this.upload(folderResource.storageKey, entries); if (!response.ok) { throw await StorageClientError.fromResponse(response); @@ -169,9 +186,16 @@ export class StorageClient { * @param signer - The signer to use for the deletion * @returns The deletion result. */ - async delete(storageKeyOrUri: string, signer: Signer): Promise { + async delete( + storageKeyOrUri: string, + signer: Signer, + ): Promise { const storageKey = extractStorageKey(storageKeyOrUri); - const authorization = await this.authorization.authorize('delete', storageKey, signer); + const authorization = await this.authorization.authorize( + 'delete', + storageKey, + signer, + ); const response = await fetch( `${this.env.backend}/${storageKey}?challenge_cid=${authorization.challengeId}&secret_random=${authorization.secret}`, @@ -239,7 +263,11 @@ export class StorageClient { options: EditFileOptions = { acl: immutable(this.env.defaultChainId) }, ): Promise { const storageKey = extractStorageKey(storageKeyOrUri); - const authorization = await this.authorization.authorize('edit', storageKey, signer); + const authorization = await this.authorization.authorize( + 'edit', + storageKey, + signer, + ); const resource = resourceFrom(storageKey, this.env); const builder = MultipartEntriesBuilder.from([resource]) @@ -269,16 +297,21 @@ export class StorageClient { try { const data = await response.json(); return statusFrom(data); - } catch (error) { + } catch (_) { throw await StorageClientError.fromResponse(response); } } - private async allocateStorage(amount: number): Promise<[Resource, ...Resource[]]> { + private async allocateStorage( + amount: number, + ): Promise<[Resource, ...Resource[]]> { invariant(amount > 0, 'Amount must be greater than 0'); - const response = await fetch(`${this.env.backend}/link/new?amount=${amount}`, { - method: 'POST', - }); + const response = await fetch( + `${this.env.backend}/link/new?amount=${amount}`, + { + method: 'POST', + }, + ); if (!response.ok) { throw await StorageClientError.fromResponse(response); @@ -286,13 +319,18 @@ export class StorageClient { return this.parseResourceFrom(response); } - private async uploadMutableFile(file: File, acl: AclConfig): Promise { + private async uploadMutableFile( + file: File, + acl: AclConfig, + ): Promise { const [resource] = await this.allocateStorage(1); - const builder = MultipartEntriesBuilder.from([resource]).withFile(file).withAclTemplate(acl); + const builder = MultipartEntriesBuilder.from([resource]) + .withFile(file) + .withAclTemplate(acl); const entries = builder.build(); - const response = await this.create(resource.storageKey, entries); + const response = await this.upload(resource.storageKey, entries); if (!response.ok) { throw await StorageClientError.fromResponse(response); @@ -300,7 +338,10 @@ export class StorageClient { return resource; } - private async uploadImmutableFile(file: File, { chainId }: ImmutableAcl): Promise { + private async uploadImmutableFile( + file: File, + { chainId }: ImmutableAcl, + ): Promise { const response = await fetch(`${this.env.backend}?chain_id=${chainId}`, { method: 'POST', headers: { @@ -317,8 +358,15 @@ export class StorageClient { return resource; } - private async create(storageKey: string, entries: readonly MultipartEntry[]): Promise { - return this.multipartRequest('POST', `${this.env.backend}/${storageKey}`, entries); + private async upload( + storageKey: string, + entries: readonly MultipartEntry[], + ): Promise { + return this.multipartRequest( + 'POST', + `${this.env.backend}/${storageKey}`, + entries, + ); } private async update( @@ -341,18 +389,23 @@ export class StorageClient { return fetch(url, await createMultipartRequestInit(method, entries)); } - private parseResourceFrom = async (response: Response): Promise<[Resource, ...Resource[]]> => { + private parseResourceFrom = async ( + response: Response, + ): Promise<[Resource, ...Resource[]]> => { const list = await response.json(); return list.map((data: Record) => { const storageKey = - data.storage_key ?? never(`Missing 'storage_key' in response: ${JSON.stringify(data)}`); + data.storage_key ?? + never(`Missing 'storage_key' in response: ${JSON.stringify(data)}`); return { storageKey, // TODO use data.gateway_url once fixed by the API // gatewayUrl: data.gateway_url ?? never('Missing gateway URL'), gatewayUrl: this.resolve(storageKey), - uri: data.uri ?? never(`Missing 'uri' in response: ${JSON.stringify(data)}`), + uri: + data.uri ?? + never(`Missing 'uri' in response: ${JSON.stringify(data)}`), }; }) as [Resource, ...Resource[]]; }; diff --git a/src/builders.ts b/src/builders.ts index e0e8480..0fd7397 100644 --- a/src/builders.ts +++ b/src/builders.ts @@ -12,7 +12,10 @@ import type { * @param address - The Wallet Address that can edit/delete the resource. * @param chainId - The Chain ID that the resource is bound to. See supported chains. */ -export function walletOnly(address: EvmAddress, chainId: number): WalletAddressAcl { +export function walletOnly( + address: EvmAddress, + chainId: number, +): WalletAddressAcl { return { template: 'wallet_address', walletAddress: address, chainId }; } @@ -22,7 +25,10 @@ export function walletOnly(address: EvmAddress, chainId: number): WalletAddressA * @param account - The Lens Account that can edit/delete the resource. * @param chainId - The Lens Chain ID that the resource is bound to. */ -export function lensAccountOnly(account: EvmAddress, chainId: number): LensAccountAcl { +export function lensAccountOnly( + account: EvmAddress, + chainId: number, +): LensAccountAcl { return { template: 'lens_account', chainId, lensAccount: account }; } diff --git a/src/errors.ts b/src/errors.ts index c92656c..7e5754e 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -5,7 +5,7 @@ class BaseError extends Error { // biome-ignore lint/complexity/noThisInStatic: need this to create the correct error type return new this(message); - } catch (error) { + } catch (_) { // biome-ignore lint/complexity/noThisInStatic: need this to create the correct error type return new this(await response.text()); } diff --git a/src/types.ts b/src/types.ts index 0c74e4c..02fab5c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ -import type { StorageClient } from './StorageClient'; import { StorageClientError } from './errors'; +import type { StorageClient } from './StorageClient'; import { delay } from './utils'; export type EvmAddress = `0x${string}`; @@ -30,7 +30,11 @@ export type WalletAddressAcl = { chainId: number; }; -export type AclConfig = GenericAcl | ImmutableAcl | LensAccountAcl | WalletAddressAcl; +export type AclConfig = + | GenericAcl + | ImmutableAcl + | LensAccountAcl + | WalletAddressAcl; /** * The marker used to identify the address of the signer attempting @@ -142,7 +146,9 @@ abstract class UploadResponse { throw StorageClientError.from(error); } } - throw StorageClientError.from(`Timeout waiting for ${this.resource.uri} to be persisted.`); + throw StorageClientError.from( + `Timeout waiting for ${this.resource.uri} to be persisted.`, + ); } } diff --git a/src/utils.ts b/src/utils.ts index dfc01fb..c9bed16 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,7 +9,10 @@ import type { AclConfig, CreateIndexContent, Resource, Status } from './types'; * @param condition - Either truthy or falsy value * @param message - An error message */ -export function invariant(condition: unknown, message: string): asserts condition { +export function invariant( + condition: unknown, + message: string, +): asserts condition { if (!condition) { throw new InvariantError(message); } @@ -45,7 +48,10 @@ export type MultipartFormDataStream = { stream: ReadableStream; }; -async function* multipartStream(entries: readonly MultipartEntry[], boundary: string) { +async function* multipartStream( + entries: readonly MultipartEntry[], + boundary: string, +) { for (const { name, file } of entries) { yield `--${boundary}\r\n`; @@ -65,7 +71,9 @@ async function* multipartStream(entries: readonly MultipartEntry[], boundary: st yield `--${boundary}--\r\n`; } -function createMultipartStream(entries: readonly MultipartEntry[]): MultipartFormDataStream { +function createMultipartStream( + entries: readonly MultipartEntry[], +): MultipartFormDataStream { const boundary = `----WebKitFormBoundary${Math.random().toString(36).substring(2)}`; return { @@ -114,7 +122,7 @@ async function detectStreamSupport(): Promise { const body = await request.text(); return duplexAccessed && !hasContentType && body === '\x00'; // 0 byte as character - } catch (error) { + } catch (_) { return false; } } @@ -129,7 +137,10 @@ function createFormData(entries: readonly MultipartEntry[]): FormData { return formData; } -function computeMultipartSize(entries: readonly MultipartEntry[], boundary: string): number { +function computeMultipartSize( + entries: readonly MultipartEntry[], + boundary: string, +): number { let size = 0; const encoder = new TextEncoder(); @@ -211,7 +222,10 @@ export function extractStorageKey(storageKeyOrUri: string): string { /** * @internal */ -export function resourceFrom(storageKey: string, env: EnvironmentConfig): Resource { +export function resourceFrom( + storageKey: string, + env: EnvironmentConfig, +): Resource { return { storageKey, gatewayUrl: `${env.backend}/${storageKey}`, @@ -276,7 +290,9 @@ function createAclEntry(template: AclConfig): MultipartEntry { }; } -function createDefaultIndexContent(files: readonly Resource[]): Record { +function createDefaultIndexContent( + files: readonly Resource[], +): Record { return { files: files.map((file) => file.storageKey), }; @@ -323,7 +339,9 @@ export class MultipartEntriesBuilder { return this; } - withIndexFile(index: CreateIndexContent | File | true): MultipartEntriesBuilder { + withIndexFile( + index: CreateIndexContent | File | true, + ): MultipartEntriesBuilder { const file = index instanceof File ? index @@ -333,7 +351,10 @@ export class MultipartEntriesBuilder { : index.call(null, this.allocations.slice()), // shallow copy ); - invariant(file.name === 'index.json', "Index file must be named 'index.json'"); + invariant( + file.name === 'index.json', + "Index file must be named 'index.json'", + ); return this.withFile(file); } From 4dce33fce72c726a7c8497990257440bec7727af Mon Sep 17 00:00:00 2001 From: Cesare Naldi Date: Tue, 19 Aug 2025 16:05:19 +0200 Subject: [PATCH 2/4] feat: support available status --- .changeset/tall-laws-tickle.md | 5 +++ src/StorageClient.test.ts | 3 +- src/StorageClient.ts | 65 +++++++++++++++++++++++++++++++--- src/environments.ts | 12 ++++--- src/types.ts | 62 +++++++++++--------------------- src/utils.ts | 14 ++++++-- vitest.config.ts | 2 +- 7 files changed, 108 insertions(+), 55 deletions(-) create mode 100644 .changeset/tall-laws-tickle.md diff --git a/.changeset/tall-laws-tickle.md b/.changeset/tall-laws-tickle.md new file mode 100644 index 0000000..5095054 --- /dev/null +++ b/.changeset/tall-laws-tickle.md @@ -0,0 +1,5 @@ +--- +"@lens-chain/storage-client": patch +--- + +**feat**: support for new `available` status diff --git a/src/StorageClient.test.ts b/src/StorageClient.test.ts index 47923d8..fda1b80 100644 --- a/src/StorageClient.test.ts +++ b/src/StorageClient.test.ts @@ -71,11 +71,12 @@ describe(`Given an instance of the '${StorageClient.name}'`, () => { describe('When testing folder uploads', () => { const files = [file1, file2]; - it('Then it should create the expected resources', async () => { + it.only('Then it should create the expected resources', async () => { const result = await client.uploadFolder(files, { acl: immutable(37111), }); + await assertFileExist(client.resolve(result.folder.uri)); await assertFileExist(client.resolve(result.files[0]?.uri ?? never())); }); diff --git a/src/StorageClient.ts b/src/StorageClient.ts index 8558ec9..321fedf 100644 --- a/src/StorageClient.ts +++ b/src/StorageClient.ts @@ -14,6 +14,7 @@ import { type Resource, type Signer, type Status, + type StatusResponse, type UploadFileOptions, type UploadFolderOptions, type UploadFolderResponse, @@ -21,13 +22,14 @@ import { } from './types'; import { createMultipartRequestInit, + delay, extractStorageKey, invariant, MultipartEntriesBuilder, type MultipartEntry, never, resourceFrom, - statusFrom, + statusResponseFrom, } from './utils'; export class StorageClient { @@ -287,7 +289,7 @@ export class StorageClient { /** * @internal */ - async status(storageKeyOrUri: string): Promise { + async status(storageKeyOrUri: string): Promise { const storageKey = extractStorageKey(storageKeyOrUri); const response = await fetch(`${this.env.backend}/status/${storageKey}`); @@ -296,12 +298,49 @@ export class StorageClient { } try { const data = await response.json(); - return statusFrom(data); + return statusResponseFrom(data); } catch (_) { throw await StorageClientError.fromResponse(response); } } + /** + * @internal + */ + async waitUntilStatus( + storageKeyOrUri: string, + expectedStatuses: Status[], + timeout: number, + ): Promise { + const startedAt = Date.now(); + + while (Date.now() - startedAt < timeout) { + const { status } = await this.status(storageKeyOrUri); + + console.log(storageKeyOrUri, status); + + // Handle common error states + switch (status) { + case 'error_upload': + case 'error_edit': + case 'error_delete': + case 'unauthorized': + throw StorageClientError.from( + `The resource ${storageKeyOrUri} has returned a '${status}' status.`, + ); + } + + if (expectedStatuses.includes(status)) { + return; + } + + await delay(this.env.statusPollingInterval); + } + throw StorageClientError.from( + `Timeout waiting for resource ${storageKeyOrUri} to reach status: ${expectedStatuses.join(' or ')}.`, + ); + } + private async allocateStorage( amount: number, ): Promise<[Resource, ...Resource[]]> { @@ -362,11 +401,19 @@ export class StorageClient { storageKey: string, entries: readonly MultipartEntry[], ): Promise { - return this.multipartRequest( + const response = await this.multipartRequest( 'POST', `${this.env.backend}/${storageKey}`, entries, ); + + await this.waitUntilStatus( + storageKey, + ['done', 'available'], + this.env.cachingTimeout, + ); + + return response; } private async update( @@ -374,11 +421,19 @@ export class StorageClient { authorization: Authorization, entries: readonly MultipartEntry[], ): Promise { - return this.multipartRequest( + const response = await this.multipartRequest( 'PUT', `${this.env.backend}/${storageKey}?challenge_cid=${authorization.challengeId}&secret_random=${authorization.secret}`, entries, ); + + await this.waitUntilStatus( + storageKey, + ['done'], + this.env.propagationTimeout, + ); + + return response; } private async multipartRequest( diff --git a/src/environments.ts b/src/environments.ts index 15c1aa1..75fad16 100644 --- a/src/environments.ts +++ b/src/environments.ts @@ -5,8 +5,9 @@ export type EnvironmentConfig = { name: string; backend: string; defaultChainId: number; + cachingTimeout: number; propagationTimeout: number; - propagationPollingInterval: number; + statusPollingInterval: number; }; /** @@ -16,8 +17,9 @@ export const production: EnvironmentConfig = { name: 'production', backend: 'https://api.grove.storage', defaultChainId: 232, + cachingTimeout: 5000, propagationTimeout: 10000, - propagationPollingInterval: 500, + statusPollingInterval: 500, }; /** @@ -27,8 +29,9 @@ export const staging: EnvironmentConfig = { name: 'staging', backend: 'https://api.staging.grove.storage', defaultChainId: 37111, + cachingTimeout: 10000, propagationTimeout: 20000, - propagationPollingInterval: 500, + statusPollingInterval: 500, }; /** @@ -38,6 +41,7 @@ export const local: EnvironmentConfig = { name: 'local', backend: 'http://localhost:30371110', defaultChainId: 37111, + cachingTimeout: 0, // no caching propagationTimeout: 30000, - propagationPollingInterval: 500, + statusPollingInterval: 500, }; diff --git a/src/types.ts b/src/types.ts index 02fab5c..fc0071a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,4 @@ -import { StorageClientError } from './errors'; import type { StorageClient } from './StorageClient'; -import { delay } from './utils'; export type EvmAddress = `0x${string}`; @@ -119,35 +117,10 @@ abstract class UploadResponse { * @throws a {@link StorageClientError} if the operation fails or times out. */ async waitForPropagation(): Promise { - const startedAt = Date.now(); - - while (Date.now() - startedAt < this.client.env.propagationTimeout) { - try { - const { status } = await this.client.status(this.resource.storageKey); - - switch (status) { - case 'done': - return; - - case 'error_upload': - case 'error_edit': - case 'error_delete': - case 'unauthorized': - throw StorageClientError.from( - `The resource ${this.resource.storageKey} has returned a '${status}' status.`, - ); - - default: - await delay(this.client.env.propagationPollingInterval); - break; - } - } catch (error) { - console.log(error); - throw StorageClientError.from(error); - } - } - throw StorageClientError.from( - `Timeout waiting for ${this.resource.uri} to be persisted.`, + return this.client.waitUntilStatus( + this.resource.storageKey, + ['done'], + this.client.env.propagationTimeout, ); } } @@ -186,7 +159,22 @@ export type DeleteResponse = { /** * @internal */ -export type Status = { +export type Status = + | 'available' + | 'dirty' + | 'done' + | 'error_delete' + | 'error_edit' + | 'error_upload' + | 'idle' + | 'new' + | 'pending' + | 'unauthorized'; + +/** + * @internal + */ +export type StatusResponse = { /** * The storage key of the resource. */ @@ -194,15 +182,7 @@ export type Status = { /** * The current status. */ - status: - | 'done' - | 'error_delete' - | 'error_edit' - | 'error_upload' - | 'idle' - | 'new' - | 'pending' - | 'unauthorized'; + status: Status; /** * A percentage value between 0-100 indicating the progress of the resource's persistence. * diff --git a/src/utils.ts b/src/utils.ts index c9bed16..3eb48c9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,12 @@ import type { EnvironmentConfig } from './environments'; import { InvariantError } from './errors'; -import type { AclConfig, CreateIndexContent, Resource, Status } from './types'; +import type { + AclConfig, + CreateIndexContent, + Resource, + Status, + StatusResponse, +} from './types'; /** * Asserts that the given condition is truthy @@ -236,9 +242,11 @@ export function resourceFrom( /** * @internal */ -export function statusFrom(data: Record): Status { +export function statusResponseFrom( + data: Record, +): StatusResponse { return { - status: data.status as Status['status'], + status: data.status as Status, storageKey: data.storage_key as string, progress: data.progress as number, }; diff --git a/vitest.config.ts b/vitest.config.ts index 930bf3c..98a93a8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ test: { setupFiles: ['./vitest.setup.ts'], env: loadEnv('', process.cwd(), ''), - testTimeout: 10000, + testTimeout: 15000, projects: [ { From 6911e70ecf172fed159d32b9ab247c4ed447f930 Mon Sep 17 00:00:00 2001 From: Cesare Naldi Date: Tue, 19 Aug 2025 16:55:01 +0200 Subject: [PATCH 3/4] chore: adds debug logs --- src/StorageClient.test.ts | 3 ++- src/StorageClient.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/StorageClient.test.ts b/src/StorageClient.test.ts index fda1b80..5546948 100644 --- a/src/StorageClient.test.ts +++ b/src/StorageClient.test.ts @@ -76,7 +76,8 @@ describe(`Given an instance of the '${StorageClient.name}'`, () => { acl: immutable(37111), }); - await assertFileExist(client.resolve(result.folder.uri)); + console.log(result); + await assertFileExist(client.resolve(result.files[0]?.uri ?? never())); }); diff --git a/src/StorageClient.ts b/src/StorageClient.ts index 321fedf..0e776f0 100644 --- a/src/StorageClient.ts +++ b/src/StorageClient.ts @@ -407,6 +407,8 @@ export class StorageClient { entries, ); + console.log('upload', `${this.env.backend}/${storageKey}`); + await this.waitUntilStatus( storageKey, ['done', 'available'], From 59785a917a8bc20a375ea7cc05c93090ab8c344f Mon Sep 17 00:00:00 2001 From: Cesare Naldi Date: Tue, 19 Aug 2025 18:03:18 +0200 Subject: [PATCH 4/4] fix: implement work around for folders without index file --- src/StorageClient.test.ts | 4 +--- src/StorageClient.ts | 31 ++++++++++++++++--------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/StorageClient.test.ts b/src/StorageClient.test.ts index 5546948..47923d8 100644 --- a/src/StorageClient.test.ts +++ b/src/StorageClient.test.ts @@ -71,13 +71,11 @@ describe(`Given an instance of the '${StorageClient.name}'`, () => { describe('When testing folder uploads', () => { const files = [file1, file2]; - it.only('Then it should create the expected resources', async () => { + it('Then it should create the expected resources', async () => { const result = await client.uploadFolder(files, { acl: immutable(37111), }); - console.log(result); - await assertFileExist(client.resolve(result.files[0]?.uri ?? never())); }); diff --git a/src/StorageClient.ts b/src/StorageClient.ts index 0e776f0..f8e094b 100644 --- a/src/StorageClient.ts +++ b/src/StorageClient.ts @@ -78,6 +78,12 @@ export class StorageClient { ? await this.uploadImmutableFile(file, acl) : await this.uploadMutableFile(file, acl); + await this.waitUntilStatus( + resource.storageKey, + ['done', 'available'], + this.env.cachingTimeout, + ); + return new FileUploadResponse(resource, this); } @@ -140,9 +146,9 @@ export class StorageClient { files: FileList | File[], options: UploadFolderOptions = { acl: immutable(this.env.defaultChainId) }, ): Promise { - const needsIndex = 'index' in options && !!options.index; + const withIndexFile = 'index' in options && !!options.index; const [folderResource, ...fileResources] = await this.allocateStorage( - files.length + (needsIndex ? 2 : 1), + files.length + (withIndexFile ? 2 : 1), ); const builder = MultipartEntriesBuilder.from(fileResources).withFiles( @@ -162,6 +168,13 @@ export class StorageClient { throw await StorageClientError.fromResponse(response); } + await this.waitUntilStatus( + // biome-ignore lint/style/noNonNullAssertion: we know the folder has at least one file + withIndexFile ? folderResource.storageKey : fileResources[0]!.storageKey, + ['done', 'available'], + this.env.cachingTimeout, + ); + return { folder: folderResource, files: fileResources, @@ -317,8 +330,6 @@ export class StorageClient { while (Date.now() - startedAt < timeout) { const { status } = await this.status(storageKeyOrUri); - console.log(storageKeyOrUri, status); - // Handle common error states switch (status) { case 'error_upload': @@ -401,21 +412,11 @@ export class StorageClient { storageKey: string, entries: readonly MultipartEntry[], ): Promise { - const response = await this.multipartRequest( + return this.multipartRequest( 'POST', `${this.env.backend}/${storageKey}`, entries, ); - - console.log('upload', `${this.env.backend}/${storageKey}`); - - await this.waitUntilStatus( - storageKey, - ['done', 'available'], - this.env.cachingTimeout, - ); - - return response; } private async update(