You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@resilientdb.apache.org by bi...@apache.org on 2026/04/11 08:14:59 UTC
(incubator-resilientdb) branch development updated: Add ResShare file-sharing toolkit under ecosystem/tools (#240)
This is an automated email from the ASF dual-hosted git repository.
bisman pushed a commit to branch development
in repository https://gitbox.apache.org/repos/asf/incubator-resilientdb.git
The following commit(s) were added to refs/heads/development by this push:
new 1dfa2c03 Add ResShare file-sharing toolkit under ecosystem/tools (#240)
1dfa2c03 is described below
commit 1dfa2c0338feef5da80224b4cfc3eeedf9538d20
Author: Devang Borkar <de...@gmail.com>
AuthorDate: Sat Apr 11 01:14:54 2026 -0700
Add ResShare file-sharing toolkit under ecosystem/tools (#240)
* Fix covery test (#246)
* fix ut
* fix ut
* Add implementation of ResShare Toolkit
- Created core files including `README.md`, `package.json`, and configuration for Jest testing.
- Implemented main client functionality in `src/index.js` and modularized authentication, file handling, and sharing features in respective modules.
- Added example scripts for basic usage and advanced scenarios.
- Included a `.gitignore` file to exclude unnecessary files from version control.
- Established a testing framework with initial unit tests for client functionality.
* Document ResShare SDK in READMEs
* Add Apache headers to ResShare toolkit files
---------
Co-authored-by: cjcchen <ic...@gmail.com>
---
.bazelversion | 1 -
README.md | 4 +-
ecosystem/README.md | 1 +
ecosystem/tools/reshare-lib/.gitignore | 21 ++
ecosystem/tools/reshare-lib/README.md | 162 +++++++++++
.../tools/reshare-lib/examples/advanced-demo.js | 256 +++++++++++++++++
.../tools/reshare-lib/examples/basic-usage.js | 41 +++
ecosystem/tools/reshare-lib/jest.config.cjs | 24 ++
ecosystem/tools/reshare-lib/package.json | 42 +++
ecosystem/tools/reshare-lib/src/index.js | 304 +++++++++++++++++++++
ecosystem/tools/reshare-lib/src/modules/auth.js | 61 +++++
ecosystem/tools/reshare-lib/src/modules/files.js | 87 ++++++
ecosystem/tools/reshare-lib/src/modules/shares.js | 68 +++++
.../tools/reshare-lib/tests/unit/client.test.js | 164 +++++++++++
platform/consensus/recovery/recovery.cpp | 8 +-
platform/consensus/recovery/recovery_test.cpp | 5 +-
16 files changed, 1240 insertions(+), 9 deletions(-)
diff --git a/.bazelversion b/.bazelversion
index 4df4a2ec..78989021 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1,3 +1,2 @@
6.0.0
-
diff --git a/README.md b/README.md
index efd5f60d..828fea00 100644
--- a/README.md
+++ b/README.md
@@ -88,7 +88,7 @@ Beyond the core NexRes engine, ResilientDB ships with a rich ecosystem of tools
- **Core Node (NexRes)**: High-throughput, BFT-replicated state machine that orders and executes transactions with strong consistency and durability.
- **APIs (GraphQL & REST)**: ResilientDB exposes HTTP and GraphQL services for committing and querying transactions, making it easy to integrate with web backends, data services, and AI workflows.
- **ResVault (Wallet & Extension)**: Browser extension wallet that manages keys, lets users authenticate, and approve transactions from web apps, tying writes on-chain to user-controlled identities.
-- **SDKs & ORMs**: Language-specific libraries (e.g., Python ORM, Rust SDK, ResVault SDK) that provide higher-level APIs for modeling assets, talking to GraphQL/REST, and wiring ResVault into applications.
+- **SDKs & ORMs**: Language-specific libraries and client toolkits (e.g., Python ORM, Rust SDK, ResVault SDK, ResShare SDK) that provide higher-level APIs for modeling assets, talking to GraphQL/REST, and wiring ResilientDB services into applications.
- **Monitoring (ResLens & dashboards)**: Monitoring UIs and middleware (ResLens stack) that surface cluster health, performance metrics, and profiling data for running deployments.
- **Deployment Tooling (Orbit, Ansible, Docker)**: Tools and playbooks for bringing up ResilientDB clusters and supporting services (GraphQL, Nginx, etc.) on local and cloud environments using Docker, Ansible, and the Orbit desktop deployer.
@@ -144,6 +144,7 @@ incubator-resilientdb/
│ └── tools/ # Dev and app tools
│ ├── create-resilient-app/ # App scaffolding CLI
│ ├── drawing-lib/ # Shared drawing widget
+│ ├── reshare-lib/ # ResShare SDK
│ └── resvault/ # ResVault wallet extension
├── executor/ # Transaction execution engine
│ ├── common/ # Common execution utilities
@@ -537,4 +538,3 @@ ResilientDB includes a built‑in smart contract service that lets you deploy an
In addition, you can use the **Key‑Value interfaces** to query or update associated balances via `get_balance` and `set_balance` commands when needed.
For a full, step‑by‑step tutorial (including example JSON files, commands, and responses), see **[Getting Started with Smart Contract on ResilientDB](https://blog.resilientdb.com/2025/02/14/GettingStartedSmartContract.html)**.
-
diff --git a/ecosystem/README.md b/ecosystem/README.md
index 13167ec3..c73b271b 100644
--- a/ecosystem/README.md
+++ b/ecosystem/README.md
@@ -55,6 +55,7 @@ ecosystem/
└── tools/
├── create-resilient-app/
├── drawing-lib/
+ ├── reshare-lib/ # ResShare SDK
└── resvault/
```
diff --git a/ecosystem/tools/reshare-lib/.gitignore b/ecosystem/tools/reshare-lib/.gitignore
new file mode 100644
index 00000000..efbe507e
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/.gitignore
@@ -0,0 +1,21 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+node_modules/
+coverage/
+examples/out/*
+!examples/out/.gitkeep
diff --git a/ecosystem/tools/reshare-lib/README.md b/ecosystem/tools/reshare-lib/README.md
new file mode 100644
index 00000000..6557bedc
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/README.md
@@ -0,0 +1,162 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+# ResShare Toolkit
+
+**License**: Apache-2.0
+
+Universal JavaScript toolkit for ResShare file-sharing APIs. This toolkit provides a modular client for authentication, file and folder operations, and share management.
+
+## Features
+
+- Authentication helpers (`signup`, `login`, `logout`, `status`, `deleteUser`)
+- File operations (`createFolder`, `upload`, `deleteItem`, `download`, `downloadZip`)
+- Share operations (`share`, `list`, `remove`)
+- Cookie-aware sessions for Node.js and browser clients
+- Retry and timeout handling with normalized API errors
+
+## Project Structure
+
+```
+reshare-lib/
+├── src/
+│ ├── index.js
+│ └── modules/
+│ ├── auth.js
+│ ├── files.js
+│ └── shares.js
+├── examples/
+│ ├── basic-usage.js
+│ └── advanced-demo.js
+├── tests/
+│ └── unit/
+├── README.md
+├── jest.config.cjs
+└── package.json
+```
+
+## Installation
+
+This toolkit is part of the Apache ResilientDB repository and is not published as a standalone package yet.
+
+```bash
+# From repository root
+npm install ./ecosystem/tools/reshare-lib
+```
+
+## Quick Start
+
+```javascript
+import ResShareToolkitClient from 'resshare-toolkit';
+
+const client = new ResShareToolkitClient({
+ baseUrl: 'http://localhost:5000'
+});
+
+await client.auth.login({ username: 'alice', password: 'secret' });
+await client.files.createFolder('docs');
+```
+
+## API Overview
+
+### Client Initialization
+
+```javascript
+const client = new ResShareToolkitClient({
+ baseUrl: 'http://localhost:5000',
+ apiPrefix: '', // Optional path prefix (for example: /api/v1)
+ timeout: 30000, // Request timeout in milliseconds
+ retries: 2, // Request retries
+ credentials: 'include', // Fetch credentials mode
+ onAuthExpired: async () => '' // Optional token refresh callback
+});
+```
+
+### Auth Module
+
+- `client.auth.login({ username, password })`
+- `client.auth.signup({ username, password })`
+- `client.auth.logout()`
+- `client.auth.status()`
+- `client.auth.deleteUser({ password })`
+
+### Files Module
+
+- `client.files.createFolder(path)`
+- `client.files.upload({ file, path, skipProcessing, filename })`
+- `client.files.deleteItem({ path, deleteInRoot })`
+- `client.files.download({ path, isShared })`
+- `client.files.downloadZip({ path, isShared })`
+
+### Shares Module
+
+- `client.shares.share({ target, path, node })`
+- `client.shares.list()`
+- `client.shares.remove({ combinedPath, fromUser, path })`
+
+## Error Handling
+
+All failed requests throw `ApiError` with:
+
+- `status`: HTTP status code
+- `body`: parsed response payload
+
+```javascript
+import ResShareToolkitClient, { ApiError } from 'resshare-toolkit';
+
+const client = new ResShareToolkitClient({ baseUrl: 'http://localhost:5000' });
+
+try {
+ await client.files.createFolder('docs');
+} catch (error) {
+ if (error instanceof ApiError) {
+ console.error(error.status, error.getUserMessage());
+ }
+}
+```
+
+## Examples
+
+```bash
+node ecosystem/tools/reshare-lib/examples/basic-usage.js
+node ecosystem/tools/reshare-lib/examples/advanced-demo.js
+```
+
+`advanced-demo.js` writes outputs to `ecosystem/tools/reshare-lib/examples/out/` during execution.
+
+## Testing
+
+```bash
+cd ecosystem/tools/reshare-lib
+npm test
+```
+
+## Backend Requirements
+
+The toolkit expects a ResShare-compatible backend exposing endpoints used by:
+
+- `/login`, `/signup`, `/logout`, `/auth-status`, `/delete-user`
+- `/create-folder`, `/upload`, `/delete`, `/download`, `/download-zip`
+- `/share`, `/shared`
+
+Configure `baseUrl` and optional `apiPrefix` to match your deployment.
+
+## License
+
+Licensed to the Apache Software Foundation (ASF) under the Apache License, Version 2.0.
diff --git a/ecosystem/tools/reshare-lib/examples/advanced-demo.js b/ecosystem/tools/reshare-lib/examples/advanced-demo.js
new file mode 100644
index 00000000..3f086cb7
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/examples/advanced-demo.js
@@ -0,0 +1,256 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import ResShareToolkitClient, { ApiError } from '../src/index.js';
+import { mkdir, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import process from 'node:process';
+
+const OUTPUT_DIR = 'ecosystem/tools/reshare-lib/examples/out';
+
+function nowSlug() {
+ const iso = new Date().toISOString();
+ const compact = iso.replace(/[-:.TZ]/g, '').slice(0, 14);
+ const random = Math.random().toString(16).slice(2, 8);
+ return `${compact}-${random}`;
+}
+
+function getErrorStatus(error) {
+ if (!(error instanceof ApiError)) return null;
+ return error.body?.status || error.body?.result || error.body?.message || null;
+}
+
+async function safeSignup(client, { username, password }) {
+ try {
+ const result = await client.auth.signup({ username, password });
+ console.log(`signup:${username}`, result.status);
+ } catch (error) {
+ const status = getErrorStatus(error);
+ if (status === 'USER_EXISTS') {
+ console.log(`signup:${username}`, status);
+ return;
+ }
+ throw error;
+ }
+}
+
+async function safeCreateFolder(client, folderPath) {
+ try {
+ const result = await client.files.createFolder(folderPath);
+ console.log(`create-folder:${folderPath}`, result.status);
+ } catch (error) {
+ const status = getErrorStatus(error);
+ if (status === 'DUPLICATE_NAME') {
+ console.log(`create-folder:${folderPath}`, status);
+ return;
+ }
+ throw error;
+ }
+}
+
+async function safeShare(client, { target, path: nodePath }) {
+ try {
+ const result = await client.shares.share({ target, path: nodePath });
+ console.log(`share:${nodePath}->${target}`, result.status);
+ } catch (error) {
+ const status = getErrorStatus(error);
+ if (status === 'ALREADY_SHARED') {
+ console.log(`share:${nodePath}->${target}`, status);
+ return;
+ }
+ throw error;
+ }
+}
+
+async function responseToFile(response, filePath) {
+ const arrayBuffer = await response.arrayBuffer();
+ const buffer = Buffer.from(arrayBuffer);
+ await writeFile(filePath, buffer);
+ return buffer.byteLength;
+}
+
+function findFirstFilePath(node, prefix = '') {
+ if (!node || typeof node !== 'object') return null;
+ const currentPath = prefix ? `${prefix}/${node.name}` : node.name;
+
+ if (!node.is_folder) {
+ return currentPath;
+ }
+
+ const children = node.children || {};
+ const childNames = Object.keys(children).sort();
+ for (const childName of childNames) {
+ const result = findFirstFilePath(children[childName], currentPath);
+ if (result) return result;
+ }
+
+ return null;
+}
+
+async function main() {
+ const runId = nowSlug();
+ const baseUrl = 'https://fc70-136-109-248-65.ngrok-free.app'; // Replace with ResilientDB ResShare backend URL
+ const password = 'Pass@123';
+ const owner = 'alice';
+ const receiver = 'bob';
+
+ await mkdir(OUTPUT_DIR, { recursive: true });
+
+ const ownerClient = new ResShareToolkitClient({ baseUrl });
+ const receiverClient = new ResShareToolkitClient({ baseUrl });
+
+ await safeSignup(ownerClient, { username: owner, password });
+ await safeSignup(receiverClient, { username: receiver, password });
+
+ const ownerLogin = await ownerClient.auth.login({ username: owner, password });
+ console.log(`login:${owner}`, ownerLogin.status);
+
+ const rootFolder = `meeting-demo-${runId}`;
+ const notesFolder = `${rootFolder}/notes`;
+ const contractsFolder = `${rootFolder}/contracts`;
+
+ await safeCreateFolder(ownerClient, rootFolder);
+ await safeCreateFolder(ownerClient, notesFolder);
+ await safeCreateFolder(ownerClient, contractsFolder);
+
+ if (typeof Blob === 'undefined') {
+ throw new Error('This example requires Blob support (Node.js >= 18).');
+ }
+
+ const meetingNotes = [
+ 'ResShare Meeting Notes',
+ `Run ID: ${runId}`,
+ 'Project: Phoenix',
+ 'Launch date: 2026-03-15',
+ 'Owner: Devang',
+ 'Budget: $125k',
+ '',
+ 'Action items:',
+ '- Upload demo artifacts',
+ '- Share folder with stakeholders',
+ '- Validate shared downloads'
+ ].join('\n');
+
+ const contractText = [
+ 'Sample Contract Summary',
+ `Run ID: ${runId}`,
+ 'Payment terms: Net 30',
+ 'Termination: 30 days notice',
+ 'Renewal: Annual'
+ ].join('\n');
+
+ const meetingNotesName = `meeting-notes-${runId}.txt`;
+ const contractName = `contract-summary-${runId}.txt`;
+
+ const uploadMeeting = await ownerClient.files.upload({
+ file: new Blob([meetingNotes], { type: 'text/plain' }),
+ filename: meetingNotesName,
+ path: notesFolder,
+ skipProcessing: false
+ });
+ console.log('upload', meetingNotesName, uploadMeeting.status, {
+ rag_processed: uploadMeeting.rag_processed,
+ rag_skipped: uploadMeeting.rag_skipped
+ });
+
+ const uploadContract = await ownerClient.files.upload({
+ file: new Blob([contractText], { type: 'text/plain' }),
+ filename: contractName,
+ path: contractsFolder,
+ skipProcessing: false
+ });
+ console.log('upload', contractName, uploadContract.status, {
+ rag_processed: uploadContract.rag_processed,
+ rag_skipped: uploadContract.rag_skipped
+ });
+
+ try {
+ const stats = await ownerClient._requestJson('/chat/stats', { method: 'GET' });
+ console.log('chat.stats', stats);
+ const chat = await ownerClient._requestJson('/chat', {
+ method: 'POST',
+ body: { query: `What is the Phoenix launch date for run ${runId}?` },
+ retries: 0
+ });
+ console.log('chat.answer', {
+ chunks_found: chat.chunks_found,
+ sources: chat.sources
+ });
+ console.log(chat.answer);
+ } catch (error) {
+ console.log('chat.error', error instanceof Error ? error.message : String(error));
+ }
+
+ const downloadPath = `${notesFolder}/${meetingNotesName}`;
+ const fileResponse = await ownerClient.files.download({ path: downloadPath });
+ const savedFile = path.join(OUTPUT_DIR, meetingNotesName);
+ const fileBytes = await responseToFile(fileResponse, savedFile);
+ console.log('download.saved', savedFile, `${fileBytes} bytes`);
+
+ const zipResponse = await ownerClient.files.downloadZip({ path: rootFolder });
+ const zipPath = path.join(OUTPUT_DIR, `${rootFolder}.zip`);
+ const zipBytes = await responseToFile(zipResponse, zipPath);
+ console.log('downloadZip.saved', zipPath, `${zipBytes} bytes`);
+
+ await safeShare(ownerClient, { target: receiver, path: rootFolder });
+
+ const receiverLogin = await receiverClient.auth.login({ username: receiver, password });
+ console.log(`login:${receiver}`, receiverLogin.status);
+
+ const shared = await receiverClient.shares.list();
+ console.log('shared.list.keys', Object.keys(shared.share_list || {}));
+
+ const fromUsers = Object.keys(shared.share_list || {}).sort();
+ const firstFromUser = fromUsers[0];
+ const firstNode = firstFromUser ? shared.share_list[firstFromUser]?.[0] : null;
+ const firstFilePath = firstNode ? findFirstFilePath(firstNode) : null;
+ if (!firstFromUser || !firstFilePath) {
+ throw new Error('No shared file discovered; share list was empty or only contained folders with no files.');
+ }
+
+ const combinedFilePath = `${firstFromUser}/${firstFilePath}`;
+ const sharedFileResponse = await receiverClient.files.download({ path: combinedFilePath, isShared: true });
+ const receiverSavedPath = path.join(OUTPUT_DIR, `shared-from-${firstFromUser}-${path.basename(firstFilePath)}`);
+ const sharedBytes = await responseToFile(sharedFileResponse, receiverSavedPath);
+ console.log('shared.download.saved', receiverSavedPath, `${sharedBytes} bytes`);
+
+ const combinedFolderPath = `${firstFromUser}/${firstNode.name}`;
+ const sharedZipResponse = await receiverClient.files.downloadZip({ path: combinedFolderPath, isShared: true });
+ const receiverZipPath = path.join(OUTPUT_DIR, `shared-from-${firstFromUser}-${firstNode.name}.zip`);
+ const sharedZipBytes = await responseToFile(sharedZipResponse, receiverZipPath);
+ console.log('shared.downloadZip.saved', receiverZipPath, `${sharedZipBytes} bytes`);
+
+ const removeResult = await receiverClient.shares.remove({ combinedPath: `${firstFromUser}/${firstNode.name}` });
+ console.log('shared.remove', removeResult.status);
+
+ await receiverClient.auth.deleteUser({ password });
+ console.log(`delete-user:${receiver}`, 'SUCCESS');
+ await ownerClient.auth.deleteUser({ password });
+ console.log(`delete-user:${owner}`, 'SUCCESS');
+}
+
+main().catch((error) => {
+ if (error instanceof ApiError) {
+ console.error('api.error', error.status, error.body);
+ process.exitCode = 1;
+ return;
+ }
+ console.error('example.failed', error instanceof Error ? error.stack || error.message : String(error));
+ process.exitCode = 1;
+});
diff --git a/ecosystem/tools/reshare-lib/examples/basic-usage.js b/ecosystem/tools/reshare-lib/examples/basic-usage.js
new file mode 100644
index 00000000..e61dd302
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/examples/basic-usage.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import ResShareToolkitClient from '../src/index.js';
+
+async function run() {
+ const client = new ResShareToolkitClient({
+ baseUrl: 'https://fc70-136-109-248-65.ngrok-free.app' // Replace with ResilientDB ResShare backend URL
+ });
+
+ await client.auth.signup({ username: 'alice', password: 'Pass@123' });
+ await client.auth.login({ username: 'alice', password: 'Pass@123' });
+
+ const folder = await client.files.createFolder('docs');
+ console.log('Create folder:', folder.status);
+
+ const shares = await client.shares.list();
+ console.log('Shared items:', shares.share_list || {});
+
+ await client.auth.logout();
+}
+
+run().catch(error => {
+ console.error('Example failed:', error);
+});
diff --git a/ecosystem/tools/reshare-lib/jest.config.cjs b/ecosystem/tools/reshare-lib/jest.config.cjs
new file mode 100644
index 00000000..53e56347
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/jest.config.cjs
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module.exports = {
+ testEnvironment: 'node',
+ roots: ['<rootDir>/tests'],
+ testMatch: ['**/*.test.js']
+};
diff --git a/ecosystem/tools/reshare-lib/package.json b/ecosystem/tools/reshare-lib/package.json
new file mode 100644
index 00000000..4c04e60c
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "resshare-toolkit",
+ "version": "0.1.0",
+ "description": "Universal JavaScript toolkit for ResShare file sharing APIs",
+ "type": "module",
+ "main": "./src/index.js",
+ "exports": {
+ ".": "./src/index.js"
+ },
+ "files": [
+ "src",
+ "README.md",
+ "examples"
+ ],
+ "scripts": {
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest"
+ },
+ "keywords": [
+ "resilientdb",
+ "reshare",
+ "file-sharing",
+ "toolkit",
+ "sdk",
+ "decentralized"
+ ],
+ "author": "ResilientDB Contributors",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/apache/incubator-resilientdb"
+ },
+ "bugs": {
+ "url": "https://github.com/apache/incubator-resilientdb/issues"
+ },
+ "homepage": "https://github.com/apache/incubator-resilientdb/tree/master/ecosystem/tools/reshare-lib",
+ "devDependencies": {
+ "jest": "^29.7.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "license": "Apache-2.0"
+}
diff --git a/ecosystem/tools/reshare-lib/src/index.js b/ecosystem/tools/reshare-lib/src/index.js
new file mode 100644
index 00000000..3ce3a503
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/src/index.js
@@ -0,0 +1,304 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import AuthClient from './modules/auth.js';
+import FilesClient from './modules/files.js';
+import SharesClient from './modules/shares.js';
+
+class ResShareToolkitClient {
+ constructor(config) {
+ if (!config || !config.baseUrl) {
+ throw new Error('ResShareToolkitClient requires a baseUrl in config');
+ }
+
+ const baseUrl = String(config.baseUrl).replace(/\/+$/, '');
+ const apiPrefix = this._normalizePrefix(config.apiPrefix);
+
+ this.config = {
+ baseUrl,
+ apiPrefix,
+ timeout: Number.isFinite(config.timeout) ? config.timeout : 30000,
+ retries: Number.isFinite(config.retries) ? config.retries : 2,
+ onAuthExpired: config.onAuthExpired || null,
+ credentials: config.credentials || 'include',
+ cookieJarEnabled: config.cookieJarEnabled
+ };
+
+ this.apiBase = `${this.config.baseUrl}${this.config.apiPrefix}`;
+
+ this._fetch = config.fetch || globalThis.fetch;
+ this._FormData = config.FormData || globalThis.FormData;
+
+ if (!this._fetch) {
+ throw new Error('ResShareToolkitClient requires fetch (provide config.fetch in Node)');
+ }
+
+ this._token = null;
+ this._cookieJarEnabled = this.config.cookieJarEnabled;
+ if (this._cookieJarEnabled === undefined) {
+ this._cookieJarEnabled = typeof window === 'undefined';
+ }
+ this._cookieStore = new Map();
+
+ this.auth = new AuthClient(this);
+ this.files = new FilesClient(this);
+ this.shares = new SharesClient(this);
+ }
+
+ setToken(token) {
+ this._token = token || null;
+ }
+
+ clearToken() {
+ this._token = null;
+ }
+
+ getToken() {
+ return this._token;
+ }
+
+ async _requestJson(path, options = {}) {
+ return this._request(path, { ...options, responseType: 'json' });
+ }
+
+ async _requestRaw(path, options = {}) {
+ return this._request(path, { ...options, responseType: 'raw' });
+ }
+
+ _createFormData() {
+ if (!this._FormData) {
+ throw new Error('FormData is not available (provide config.FormData in Node)');
+ }
+ return new this._FormData();
+ }
+
+ _buildUrl(path) {
+ if (!path) {
+ return this.apiBase;
+ }
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
+ return `${this.apiBase}${normalizedPath}`;
+ }
+
+ _buildCookieHeader() {
+ if (!this._cookieJarEnabled || !this._cookieStore.size) {
+ return '';
+ }
+ return Array.from(this._cookieStore.entries())
+ .map(([name, value]) => `${name}=${value}`)
+ .join('; ');
+ }
+
+ _storeCookies(response) {
+ if (!this._cookieJarEnabled || !response?.headers) {
+ return;
+ }
+
+ let cookies = [];
+ if (typeof response.headers.getSetCookie === 'function') {
+ cookies = response.headers.getSetCookie();
+ } else {
+ const header = response.headers.get('set-cookie');
+ if (header) {
+ cookies = [header];
+ }
+ }
+
+ cookies.forEach((cookie) => {
+ const pair = cookie.split(';')[0];
+ const separatorIndex = pair.indexOf('=');
+ if (separatorIndex <= 0) {
+ return;
+ }
+ const name = pair.slice(0, separatorIndex).trim();
+ const value = pair.slice(separatorIndex + 1).trim();
+ if (name) {
+ this._cookieStore.set(name, value);
+ }
+ });
+ }
+
+ _normalizePrefix(prefix) {
+ if (!prefix) {
+ return '';
+ }
+ const trimmed = String(prefix).trim();
+ if (!trimmed) {
+ return '';
+ }
+ const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
+ return withSlash.replace(/\/+$/, '');
+ }
+
+ _normalizeResponse(data) {
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
+ return data;
+ }
+
+ if (data.status == null) {
+ const status = data.result || data.message;
+ if (status) {
+ return { ...data, status };
+ }
+ }
+
+ return data;
+ }
+
+ async _request(path, options = {}) {
+ const url = this._buildUrl(path);
+ const method = options.method || 'GET';
+ const headers = { ...(options.headers || {}) };
+ const maxRetries = Number.isFinite(options.retries) ? options.retries : this.config.retries;
+ const responseType = options.responseType || 'json';
+
+ let body = options.body;
+ const isFormData = this._FormData && body instanceof this._FormData;
+ const isString = typeof body === 'string';
+ const isBlob = typeof Blob !== 'undefined' && body instanceof Blob;
+ const isBinary = isBlob || body instanceof ArrayBuffer || body instanceof Uint8Array;
+
+ if (body != null && !isFormData && !isString && !isBinary) {
+ if (!headers['Content-Type']) {
+ headers['Content-Type'] = 'application/json';
+ }
+ body = JSON.stringify(body);
+ }
+
+ if (this._token && !headers.Authorization) {
+ headers.Authorization = `Bearer ${this._token}`;
+ }
+
+ const cookieHeader = this._buildCookieHeader();
+ if (cookieHeader) {
+ if (headers.Cookie) {
+ headers.Cookie = `${headers.Cookie}; ${cookieHeader}`;
+ } else {
+ headers.Cookie = cookieHeader;
+ }
+ }
+
+ const fetchOptions = {
+ method,
+ headers,
+ body,
+ credentials: options.credentials || this.config.credentials
+ };
+
+ let lastError = null;
+ let refreshed = false;
+
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
+ try {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
+
+ const response = await this._fetch(url, {
+ ...fetchOptions,
+ signal: controller.signal
+ });
+
+ clearTimeout(timeoutId);
+ this._storeCookies(response);
+
+ if (response.ok) {
+ if (responseType === 'raw') {
+ return response;
+ }
+
+ const contentType = response.headers.get('content-type') || '';
+ if (contentType.includes('application/json')) {
+ const data = await response.json();
+ return this._normalizeResponse(data);
+ }
+
+ return null;
+ }
+
+ let errorBody = null;
+ try {
+ errorBody = await response.json();
+ } catch (error) {
+ errorBody = { message: response.statusText };
+ }
+
+ if (response.status === 401 && this.config.onAuthExpired && !refreshed) {
+ refreshed = true;
+ const newToken = await this.config.onAuthExpired();
+ if (newToken) {
+ this.setToken(newToken);
+ attempt -= 1;
+ continue;
+ }
+ }
+
+ throw new ApiError(response.status, this._normalizeResponse(errorBody));
+ } catch (error) {
+ lastError = error;
+
+ if (error instanceof ApiError) {
+ if ([400, 401, 403, 404].includes(error.status)) {
+ throw error;
+ }
+ }
+
+ if (attempt === maxRetries) {
+ break;
+ }
+
+ await this._sleep(Math.pow(2, attempt) * 250);
+ }
+ }
+
+ throw lastError || new Error('Request failed after retries');
+ }
+
+ _sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+}
+
+class ApiError extends Error {
+ constructor(status, body, message = null) {
+ super(message || body?.message || body?.status || `API error: ${status}`);
+ this.name = 'ApiError';
+ this.status = status;
+ this.body = body;
+ }
+
+ isAuthError() {
+ return this.status === 401;
+ }
+
+ isValidationError() {
+ return this.status === 400;
+ }
+
+ getUserMessage() {
+ if (this.body?.errors && typeof this.body.errors === 'object') {
+ return Object.entries(this.body.errors)
+ .map(([field, msg]) => `${field}: ${msg}`)
+ .join(', ');
+ }
+ return this.body?.message || this.message;
+ }
+}
+
+export { ResShareToolkitClient, ApiError };
+export default ResShareToolkitClient;
diff --git a/ecosystem/tools/reshare-lib/src/modules/auth.js b/ecosystem/tools/reshare-lib/src/modules/auth.js
new file mode 100644
index 00000000..5fc74258
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/src/modules/auth.js
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+class AuthClient {
+ constructor(client) {
+ this.client = client;
+ }
+
+ async login({ username, password }) {
+ return this.client._requestJson('/login', {
+ method: 'POST',
+ body: { username, password },
+ retries: 0
+ });
+ }
+
+ async signup({ username, password }) {
+ return this.client._requestJson('/signup', {
+ method: 'POST',
+ body: { username, password },
+ retries: 0
+ });
+ }
+
+ async logout() {
+ return this.client._requestJson('/logout', {
+ method: 'POST'
+ });
+ }
+
+ async deleteUser({ password }) {
+ return this.client._requestJson('/delete-user', {
+ method: 'DELETE',
+ body: { password }
+ });
+ }
+
+ async status() {
+ return this.client._requestJson('/auth-status', {
+ method: 'GET'
+ });
+ }
+}
+
+export default AuthClient;
diff --git a/ecosystem/tools/reshare-lib/src/modules/files.js b/ecosystem/tools/reshare-lib/src/modules/files.js
new file mode 100644
index 00000000..d85eb0e4
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/src/modules/files.js
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+class FilesClient {
+ constructor(client) {
+ this.client = client;
+ }
+
+ async createFolder(path) {
+ return this.client._requestJson('/create-folder', {
+ method: 'POST',
+ body: { folder_path: path }
+ });
+ }
+
+ async upload({ file, path = '', skipProcessing = true, filename }) {
+ if (!file) {
+ throw new Error('upload requires a file');
+ }
+
+ const formData = this.client._createFormData();
+ const resolvedName = filename || file.name;
+
+ if (resolvedName) {
+ formData.append('file', file, resolvedName);
+ } else {
+ formData.append('file', file);
+ }
+
+ formData.append('path', path || '');
+ formData.append('skip_ai_processing', String(Boolean(skipProcessing)));
+
+ return this.client._requestJson('/upload', {
+ method: 'POST',
+ body: formData,
+ retries: 0
+ });
+ }
+
+ async deleteItem({ path, deleteInRoot = true }) {
+ return this.client._requestJson('/delete', {
+ method: 'DELETE',
+ body: {
+ node_path: path,
+ delete_in_root: deleteInRoot
+ }
+ });
+ }
+
+ async download({ path, isShared = false }) {
+ return this.client._requestRaw('/download', {
+ method: 'POST',
+ body: {
+ path,
+ is_shared: isShared
+ }
+ });
+ }
+
+ async downloadZip({ path, isShared = false }) {
+ return this.client._requestRaw('/download-zip', {
+ method: 'POST',
+ body: {
+ path,
+ is_shared: isShared
+ }
+ });
+ }
+}
+
+export default FilesClient;
diff --git a/ecosystem/tools/reshare-lib/src/modules/shares.js b/ecosystem/tools/reshare-lib/src/modules/shares.js
new file mode 100644
index 00000000..17ee9b0b
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/src/modules/shares.js
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+class SharesClient {
+ constructor(client) {
+ this.client = client;
+ }
+
+ async share({ target, path, node }) {
+ if (!target) {
+ throw new Error('share requires a target username');
+ }
+
+ const payload = { target };
+
+ if (path) {
+ payload.path = path;
+ }
+
+ if (node) {
+ payload.node = typeof node === 'string' ? node : JSON.stringify(node);
+ }
+
+ return this.client._requestJson('/share', {
+ method: 'POST',
+ body: payload
+ });
+ }
+
+ async list() {
+ return this.client._requestJson('/shared', {
+ method: 'GET'
+ });
+ }
+
+ async remove({ combinedPath, fromUser, path }) {
+ const nodePath = combinedPath || (fromUser && path ? `${fromUser}/${path}` : null);
+ if (!nodePath) {
+ throw new Error('remove requires combinedPath or fromUser and path');
+ }
+
+ return this.client._requestJson('/delete', {
+ method: 'DELETE',
+ body: {
+ node_path: nodePath,
+ delete_in_root: false
+ }
+ });
+ }
+}
+
+export default SharesClient;
diff --git a/ecosystem/tools/reshare-lib/tests/unit/client.test.js b/ecosystem/tools/reshare-lib/tests/unit/client.test.js
new file mode 100644
index 00000000..21dc51e0
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/tests/unit/client.test.js
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import ResShareToolkitClient from '../../src/index.js';
+
+class MockFormData {
+ constructor() {
+ this.fields = [];
+ }
+
+ append(name, value, filename) {
+ this.fields.push({ name, value, filename });
+ }
+}
+
+const jsonResponse = (data, status = 200) => ({
+ ok: status >= 200 && status < 300,
+ status,
+ headers: {
+ get: (name) => (name.toLowerCase() === 'content-type' ? 'application/json' : null)
+ },
+ json: async () => data
+});
+
+const jsonResponseWithCookies = (data, cookies, status = 200) => ({
+ ok: status >= 200 && status < 300,
+ status,
+ headers: {
+ get: (name) => (name.toLowerCase() === 'content-type' ? 'application/json' : null),
+ getSetCookie: () => cookies
+ },
+ json: async () => data
+});
+
+describe('ResShareToolkitClient', () => {
+ test('requires baseUrl', () => {
+ expect(() => new ResShareToolkitClient({})).toThrow(/baseUrl/);
+ });
+
+ test('builds apiBase with prefix', () => {
+ const client = new ResShareToolkitClient({
+ baseUrl: 'http://localhost:5000/',
+ apiPrefix: '/api/v1',
+ fetch: async () => jsonResponse({}),
+ FormData: MockFormData
+ });
+
+ expect(client.apiBase).toBe('http://localhost:5000/api/v1');
+ });
+
+ test('login sends JSON body and credentials', async () => {
+ let captured = null;
+ const fetch = async (url, options) => {
+ captured = { url, options };
+ return jsonResponse({ message: 'SUCCESS' });
+ };
+
+ const client = new ResShareToolkitClient({
+ baseUrl: 'http://localhost:5000',
+ fetch,
+ FormData: MockFormData
+ });
+
+ await client.auth.login({ username: 'alice', password: 'secret' });
+
+ expect(captured.url).toBe('http://localhost:5000/login');
+ expect(captured.options.method).toBe('POST');
+ expect(captured.options.credentials).toBe('include');
+ expect(captured.options.headers['Content-Type']).toBe('application/json');
+ expect(JSON.parse(captured.options.body)).toEqual({
+ username: 'alice',
+ password: 'secret'
+ });
+ });
+
+ test('upload builds form data payload', async () => {
+ let captured = null;
+ const fetch = async (url, options) => {
+ captured = { url, options };
+ return jsonResponse({ message: 'SUCCESS' });
+ };
+
+ const client = new ResShareToolkitClient({
+ baseUrl: 'http://localhost:5000',
+ fetch,
+ FormData: MockFormData
+ });
+
+ const file = { name: 'doc.txt' };
+ await client.files.upload({ file, path: 'docs', skipProcessing: true });
+
+ expect(captured.url).toBe('http://localhost:5000/upload');
+ expect(captured.options.method).toBe('POST');
+ expect(captured.options.body instanceof MockFormData).toBe(true);
+
+ const fields = captured.options.body.fields;
+ const fieldMap = Object.fromEntries(fields.map((field) => [field.name, field]));
+
+ expect(fieldMap.path.value).toBe('docs');
+ expect(fieldMap.skip_ai_processing.value).toBe('true');
+ expect(fieldMap.file.filename).toBe('doc.txt');
+ });
+
+ test('remove share builds combined path', async () => {
+ let captured = null;
+ const fetch = async (url, options) => {
+ captured = { url, options };
+ return jsonResponse({ message: 'SUCCESS' });
+ };
+
+ const client = new ResShareToolkitClient({
+ baseUrl: 'http://localhost:5000',
+ fetch,
+ FormData: MockFormData
+ });
+
+ await client.shares.remove({ fromUser: 'alice', path: 'docs' });
+
+ expect(captured.url).toBe('http://localhost:5000/delete');
+ expect(captured.options.method).toBe('DELETE');
+ expect(JSON.parse(captured.options.body)).toEqual({
+ node_path: 'alice/docs',
+ delete_in_root: false
+ });
+ });
+
+ test('persists session cookie between requests in Node', async () => {
+ const calls = [];
+ const fetch = async (url, options) => {
+ calls.push({ url, options });
+ if (calls.length === 1) {
+ return jsonResponseWithCookies({ message: 'SUCCESS' }, ['session=abc123; Path=/; HttpOnly']);
+ }
+ return jsonResponse({ message: 'SUCCESS' });
+ };
+
+ const client = new ResShareToolkitClient({
+ baseUrl: 'http://localhost:5000',
+ fetch,
+ FormData: MockFormData
+ });
+
+ await client.auth.login({ username: 'alice', password: 'secret' });
+ await client.files.createFolder('docs');
+
+ expect(calls[1].options.headers.Cookie).toBe('session=abc123');
+ });
+});
diff --git a/platform/consensus/recovery/recovery.cpp b/platform/consensus/recovery/recovery.cpp
index bf24ebc7..9e37f6eb 100644
--- a/platform/consensus/recovery/recovery.cpp
+++ b/platform/consensus/recovery/recovery.cpp
@@ -452,9 +452,11 @@ void Recovery::ReadLogs(
if (recovery_enabled_ == false) {
return;
}
- assert(storage_);
- int64_t storage_ckpt = storage_->GetLastCheckpoint();
- LOG(ERROR) << " storage ckpt:" << storage_ckpt;
+
+ int64_t storage_ckpt = 0;
+ if(storage_) {
+ storage_ckpt = storage_->GetLastCheckpoint();
+ }
std::unique_lock<std::mutex> lk(mutex_);
auto recovery_files_pair = GetRecoveryFiles(storage_ckpt);
diff --git a/platform/consensus/recovery/recovery_test.cpp b/platform/consensus/recovery/recovery_test.cpp
index 6c0dd25b..f6aeba47 100644
--- a/platform/consensus/recovery/recovery_test.cpp
+++ b/platform/consensus/recovery/recovery_test.cpp
@@ -83,7 +83,7 @@ TEST_F(RecoveryTest, ReadLog) {
std::vector<int> expected_types = {
Request::TYPE_PRE_PREPARE, Request::TYPE_PREPARE, Request::TYPE_COMMIT,
- Request::TYPE_CHECKPOINT, Request::TYPE_NEWVIEW,
+ Request::TYPE_NEWVIEW,
};
{
@@ -123,7 +123,7 @@ TEST_F(RecoveryTest, ReadLog_FlushOnce) {
std::vector<int> expected_types = {
Request::TYPE_PRE_PREPARE, Request::TYPE_PREPARE, Request::TYPE_COMMIT,
- Request::TYPE_CHECKPOINT, Request::TYPE_NEWVIEW,
+ Request::TYPE_NEWVIEW,
};
{
@@ -142,7 +142,6 @@ TEST_F(RecoveryTest, ReadLog_FlushOnce) {
recovery.ReadLogs([&](const SystemInfoData &data) {},
[&](std::unique_ptr<Context> context,
std::unique_ptr<Request> request) {
- LOG(ERROR) << "call back:" << request->seq();
list.push_back(*request);
},
nullptr);