bloom-filter-driver 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +321 -0
- package/dist/BloomApiClient.d.ts +13 -0
- package/dist/BloomApiClient.js +59 -0
- package/dist/BloomFilterDriver.d.ts +106 -0
- package/dist/BloomFilterDriver.js +144 -0
- package/dist/DriverValidator.d.ts +16 -0
- package/dist/DriverValidator.js +96 -0
- package/dist/HttpClient.d.ts +10 -0
- package/dist/HttpClient.js +66 -0
- package/dist/RangeApiClient.d.ts +15 -0
- package/dist/RangeApiClient.js +68 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +14 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +23 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.js +2 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Bloom Filter Driver (TypeScript / Node.js HTTP Client)
|
|
2
|
+
|
|
3
|
+
**BloomFilterDriver** là thư viện client (SDK) nhỏ gọn được viết bằng TypeScript, đóng vai trò cầu nối giao tiếp giữa các ứng dụng Node.js/TypeScript (như `BloomFilterApplication`) và máy chủ xử lý dữ liệu Bloom Filter (`BloomFilterProcess`) thông qua giao thức HTTP REST.
|
|
4
|
+
|
|
5
|
+
Thư viện được thiết kế độc lập, tuân thủ nguyên tắc kiến trúc sạch (Clean Architecture) với các lớp phân tách rõ ràng, hỗ trợ đầy đủ kiểu dữ liệu TypeScript (Type Safety), cơ chế kiểm tra tính hợp lệ dữ liệu (Validation) chặt chẽ và khả năng quản lý vòng đời request (Timeout & Cancellation) thông qua `AbortSignal`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 📦 Cài đặt
|
|
10
|
+
|
|
11
|
+
Bạn có thể cài đặt thư viện thông qua các công cụ quản lý gói thông dụng:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install bloom-filter-driver
|
|
15
|
+
# hoặc
|
|
16
|
+
yarn add bloom-filter-driver
|
|
17
|
+
# hoặc
|
|
18
|
+
pnpm add bloom-filter-driver
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> **Lưu ý khi phát triển cục bộ (Local Development):**
|
|
22
|
+
> Nếu bạn đang làm việc trực tiếp trong repository chứa mã nguồn của driver, hãy cài đặt các gói phụ thuộc và biên dịch mã nguồn trước khi sử dụng hoặc liên kết (link) tới ứng dụng:
|
|
23
|
+
> ```bash
|
|
24
|
+
> npm install
|
|
25
|
+
> npm run build
|
|
26
|
+
> ```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 🚀 Khởi tạo & Cấu hình Driver
|
|
31
|
+
|
|
32
|
+
Để sử dụng, bạn cần khởi tạo một đối tượng `BloomFilterDriver` với cấu hình `BloomDriverConfig`.
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { BloomFilterDriver } from 'bloom-filter-driver';
|
|
36
|
+
|
|
37
|
+
const driver = new BloomFilterDriver({
|
|
38
|
+
baseUrl: 'http://localhost:8080', // Địa chỉ gốc của máy chủ BloomFilterProcess
|
|
39
|
+
timeoutMs: 5000, // (Tùy chọn) Thời gian chờ tối đa cho mỗi request (mặc định: 5000ms)
|
|
40
|
+
// fetchImpl: customFetch // (Tùy chọn) Hàm fetch tùy chỉnh (hữu ích khi viết mock/unit test)
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Quản lý vòng đời Request (Timeout & Cancellation)
|
|
45
|
+
|
|
46
|
+
Tất cả các phương thức API trong driver đều hỗ trợ tham số tùy chọn `options?: { signal?: AbortSignal }`. Điều này cho phép bạn:
|
|
47
|
+
- Tự động hủy các request vượt quá thời gian `timeoutMs` đã cấu hình.
|
|
48
|
+
- Chủ động hủy request từ bên ngoài thông qua `AbortController`.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await driver.add(
|
|
55
|
+
{ bfName: 'users-bf', data: 'alice@example.com' },
|
|
56
|
+
{ signal: controller.signal }
|
|
57
|
+
);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Xử lý khi request bị hủy hoặc lỗi
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Khi muốn chủ động hủy request đang thực thi:
|
|
63
|
+
controller.abort();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 🏗️ Kiến trúc & Thiết kế
|
|
69
|
+
|
|
70
|
+
Driver được phân chia thành các module chuyên biệt nhằm đảm bảo tính dễ bảo trì và mở rộng:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
┌─────────────────────────────────────────────────────────┐
|
|
74
|
+
│ BloomFilterDriver │ (Lớp Facade tổng quan)
|
|
75
|
+
└────────────┬───────────────────────────────┬────────────┘
|
|
76
|
+
│ │
|
|
77
|
+
▼ ▼
|
|
78
|
+
┌─────────────────────────┐ ┌─────────────────────────┐
|
|
79
|
+
│ BloomApiClient │ │ RangeApiClient │ (Lớp Client nghiệp vụ)
|
|
80
|
+
└────────────┬────────────┘ └────────────┬────────────┘
|
|
81
|
+
│ │
|
|
82
|
+
└───────────────┬───────────────┘
|
|
83
|
+
│
|
|
84
|
+
▼
|
|
85
|
+
┌─────────────────────────────────────────────────────────┐
|
|
86
|
+
│ HttpClient │ (Lớp xử lý HTTP & Timeout)
|
|
87
|
+
└────────────────────────────┬────────────────────────────┘
|
|
88
|
+
│
|
|
89
|
+
▼
|
|
90
|
+
┌─────────────────────────────────────────────────────────┐
|
|
91
|
+
│ DriverValidator │ (Lớp kiểm tra dữ liệu)
|
|
92
|
+
└─────────────────────────────────────────────────────────┘
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- **`BloomFilterDriver`**: Lớp giao diện chính (Facade) tổng hợp toàn bộ các phương thức thao tác với Bloom Filter và RangeLH.
|
|
96
|
+
- **`BloomApiClient`**: Quản lý các endpoint liên quan đến cấu trúc Bloom Filter (`/bloom/*`).
|
|
97
|
+
- **`RangeApiClient`**: Quản lý các endpoint liên quan đến cấu trúc phân cấp/chỉ mục dải RangeLH (`/rangelh/*`).
|
|
98
|
+
- **`HttpClient`**: Đóng gói hàm `fetch`, xử lý nối chuỗi URL, thiết lập HTTP headers, quản lý `AbortController` và chuẩn hóa lỗi HTTP.
|
|
99
|
+
- **`DriverValidator`**: Kiểm tra tính hợp lệ của dữ liệu đầu vào (tên bộ lọc, dải giá trị, xác suất, tham số cấu hình) trước khi gửi request tới máy chủ.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 📚 Tài liệu API Tham khảo (API Reference)
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### 1. Bloom Filter API (`BloomApiClient`)
|
|
108
|
+
|
|
109
|
+
Các API chuyên biệt dành cho bộ lọc Bloom thông thường (Standard Bloom Filter) và bộ lọc Bloom có đếm (Counting Bloom Filter).
|
|
110
|
+
|
|
111
|
+
#### `createBloom(payload: BloomCreatePayload, options?: DriverRequestOptions): Promise<void>`
|
|
112
|
+
Khởi tạo và cấu hình một bộ lọc Bloom mới trên máy chủ trước khi sử dụng.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
await driver.createBloom({
|
|
116
|
+
bfName: 'users-bf', // Tên định danh của bộ lọc
|
|
117
|
+
expectedValues: 100000, // Số lượng phần tử dự kiến thêm vào
|
|
118
|
+
bloomType: 'standard', // 'standard' | 'counting' (mặc định: 'standard')
|
|
119
|
+
falsePositiveProbability: 0.01, // (Tùy chọn) Xác suất dương tính giả mong muốn (ví dụ: 1%)
|
|
120
|
+
counterBits: 4 // (Tùy chọn) Số bit cho mỗi bộ đếm (chỉ dùng khi bloomType là 'counting', từ 1-8)
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
> *Lưu ý:* Thư viện cung cấp hàm alias `configureBloom` tương đương với `createBloom` nhằm mục đích tương thích ngược. Khuyến nghị sử dụng `createBloom` cho các dự án mới.
|
|
124
|
+
|
|
125
|
+
#### `add(payload: BloomDataPayload, options?: DriverRequestOptions): Promise<void>`
|
|
126
|
+
Thêm một phần tử (chuỗi dữ liệu) vào bộ lọc Bloom.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
await driver.add({
|
|
130
|
+
bfName: 'users-bf',
|
|
131
|
+
data: 'alice@example.com'
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### `mightContain(payload: BloomDataPayload, options?: DriverRequestOptions): Promise<boolean>`
|
|
136
|
+
Kiểm tra xem một phần tử **có thể** tồn tại trong bộ lọc Bloom hay không.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const isPresent = await driver.mightContain({
|
|
140
|
+
bfName: 'users-bf',
|
|
141
|
+
data: 'alice@example.com'
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (isPresent) {
|
|
145
|
+
console.log('Phần tử CÓ THỂ tồn tại (có rủi ro dương tính giả theo xác suất đã cấu hình).');
|
|
146
|
+
} else {
|
|
147
|
+
console.log('Phần tử CHẮC CHẮN KHÔNG tồn tại trong bộ lọc.');
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### `remove(payload: BloomDataPayload, options?: DriverRequestOptions): Promise<void>`
|
|
152
|
+
Xóa một phần tử khỏi bộ lọc Bloom. (Yêu cầu bộ lọc phải được khởi tạo với `bloomType: 'counting'`).
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
await driver.remove({
|
|
156
|
+
bfName: 'users-bf',
|
|
157
|
+
data: 'alice@example.com'
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### `benchLoadSequentialKeys(payload: BloomBenchSequentialPayload, options?: DriverRequestOptions): Promise<void>`
|
|
162
|
+
API hỗ trợ nạp dữ liệu giả lập hàng loạt (bulk insert) trực tiếp trên máy chủ với hiệu năng cao (chỉ tốn 1 lần gọi HTTP và 1 lần lưu trữ/persist). Thường dùng cho mục đích benchmark hoặc khởi tạo dữ liệu mẫu.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
await driver.benchLoadSequentialKeys({
|
|
166
|
+
bfName: 'users-bf',
|
|
167
|
+
from: 1, // (Tùy chọn) Chỉ số bắt đầu (mặc định: 1)
|
|
168
|
+
to: 100000, // Chỉ số kết thúc
|
|
169
|
+
keyWidth: 10 // (Tùy chọn) Độ rộng chuỗi số được đệm zero (zero-padding, mặc định: 10)
|
|
170
|
+
});
|
|
171
|
+
// Máy chủ sẽ tự động nạp các key dạng: "k-0000000001", "k-0000000002", ..., "k-0000100000"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
### 2. RangeLH API (`RangeApiClient`)
|
|
177
|
+
|
|
178
|
+
RangeLH (Range Linear Hashing) là cấu trúc chỉ mục nâng cao phân bổ theo `key`, hỗ trợ tối ưu hóa các truy vấn điểm (point query), truy vấn khoảng (range query) và tổng hợp dữ liệu (aggregation).
|
|
179
|
+
|
|
180
|
+
#### `createRange(payload: RangeCreatePayload, options?: DriverRequestOptions): Promise<void>`
|
|
181
|
+
Khởi tạo cấu trúc chỉ mục RangeLH với các tham số tinh chỉnh chuyên sâu.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
await driver.createRange({
|
|
185
|
+
bfName: 'orders-range',
|
|
186
|
+
config: {
|
|
187
|
+
masterInitNumBucket: 2, // Số lượng bucket khởi tạo ban đầu cho Master Index
|
|
188
|
+
masterSplitPolicy: 0.5, // Tỷ lệ/Chính sách tách bucket (split policy)
|
|
189
|
+
expectedNItems: 1000000, // Tổng số lượng phần tử dự kiến
|
|
190
|
+
fpProbBloomRF: 0.01, // Xác suất dương tính giả cho các Bloom Filter con
|
|
191
|
+
deltaBloomRF: 10, // Tham số dải delta cho bộ lọc
|
|
192
|
+
keyLength: 20, // Độ dài cố định của chuỗi key (từ 1-64)
|
|
193
|
+
maxBytesString: 8, // Số byte tối đa cho phần chuỗi giá trị (từ 1-8)
|
|
194
|
+
floatScale: 100 // Tỷ lệ nhân (scale) cho dữ liệu số thực
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### `addRange(payload: RangePayload, options?: Driver: RequestOptions): Promise<void>`
|
|
200
|
+
Thêm một cặp định danh/khóa (key) đơn lẻ vào chỉ mục RangeLH.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
await driver.addRange({
|
|
204
|
+
bfName: 'orders-range',
|
|
205
|
+
key: '2026-05-17T12:00:00Z'
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### `addRangeBulk(payload: RangeBulkAddPayload, options?: DriverRequestOptions): Promise<void>`
|
|
210
|
+
Thêm nhiều khóa cùng lúc (bulk insert) vào chỉ mục RangeLH, giúp tiết kiệm băng thông và giảm thiểu độ trễ mạng.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
await driver.addRangeBulk({
|
|
214
|
+
bfName: 'orders-range',
|
|
215
|
+
keys: [
|
|
216
|
+
'2026-05-17T12:00:00Z',
|
|
217
|
+
'2026-05-17T13:00:00Z',
|
|
218
|
+
'2026-05-17T14:00:00Z'
|
|
219
|
+
]
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### `removeRange(payload: RangePayload, options?: DriverRequestOptions): Promise<void>`
|
|
224
|
+
Xóa một khóa khỏi chỉ mục RangeLH.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
await driver.removeRange({
|
|
228
|
+
bfName: 'orders-range',
|
|
229
|
+
key: '2026-05-17T12:00:00Z'
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### `pointQuery(payload: PointQueryPayload, options?: DriverRequestOptions): Promise<string[]>`
|
|
234
|
+
Truy vấn điểm để lấy ra danh sách các khóa khớp chính xác.
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
const results = await driver.pointQuery({
|
|
238
|
+
bfName: 'orders-range',
|
|
239
|
+
key: '2026-05-17T12:00:00Z'
|
|
240
|
+
});
|
|
241
|
+
// results: string[] (Danh sách các giá trị/khóa tìm thấy)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### `rangeQuery(payload: RangeQueryPayload, options?: DriverRequestOptions): Promise<string[]>`
|
|
245
|
+
Truy vấn khoảng để lấy ra danh sách các khóa nằm trong dải từ `from` đến `to`.
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
const results = await driver.rangeQuery({
|
|
249
|
+
bfName: 'orders-range',
|
|
250
|
+
from: '2026-05-01T00:00:00Z',
|
|
251
|
+
to: '2026-05-31T23:59:59Z'
|
|
252
|
+
});
|
|
253
|
+
// results: string[] (Danh sách các khóa thuộc khoảng thời gian/dải truy vấn)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### `aggregate(payload: RangeAggregatePayload, options?: DriverRequestOptions): Promise<number>`
|
|
257
|
+
Thực hiện truy vấn tổng hợp (aggregation) dữ liệu trên một dải khóa cụ thể.
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
const totalOrders = await driver.aggregate({
|
|
261
|
+
bfName: 'orders-range',
|
|
262
|
+
from: '2026-05-01T00:00:00Z',
|
|
263
|
+
to: '2026-05-31T23:59:59Z',
|
|
264
|
+
op: 'count' // Các phép toán hỗ trợ: 'sum' | 'avg' | 'min' | 'max' | 'count'
|
|
265
|
+
});
|
|
266
|
+
console.log(`Tổng số đơn hàng trong tháng 5: ${totalOrders}`);
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## ⚠️ Xử lý lỗi (Error Handling)
|
|
272
|
+
|
|
273
|
+
Tất cả các lỗi phát sinh từ driver (bao gồm lỗi kiểm tra dữ liệu đầu vào, lỗi mạng, lỗi timeout hoặc lỗi nghiệp vụ từ phía máy chủ) đều được ném ra dưới dạng đối tượng `BloomDriverError`.
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { BloomFilterDriver, BloomDriverError } from 'bloom-filter-driver';
|
|
277
|
+
|
|
278
|
+
const driver = new BloomFilterDriver({ baseUrl: 'http://localhost:8080' });
|
|
279
|
+
|
|
280
|
+
async function execute() {
|
|
281
|
+
try {
|
|
282
|
+
// Thử gửi dữ liệu không hợp lệ (chuỗi rỗng)
|
|
283
|
+
await driver.add({ bfName: 'users-bf', data: '' });
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (error instanceof BloomDriverError) {
|
|
286
|
+
console.error('[BloomDriverError] Thông báo lỗi:', error.message);
|
|
287
|
+
|
|
288
|
+
if (error.status) {
|
|
289
|
+
console.error('Mã trạng thái HTTP:', error.status);
|
|
290
|
+
}
|
|
291
|
+
if (error.responseBody) {
|
|
292
|
+
console.error('Nội dung phản hồi từ máy chủ:', error.responseBody);
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
console.error('[Lỗi không xác định]:', error);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## 🛠️ Build & Publish (Dành cho nhà phát triển)
|
|
304
|
+
|
|
305
|
+
Các lệnh thao tác chính khi tham gia phát triển gói thư viện `bloom-filter-driver`:
|
|
306
|
+
|
|
307
|
+
- **Kiểm tra lỗi cú pháp và định dạng (Lint & Format):**
|
|
308
|
+
```bash
|
|
309
|
+
npm run lint
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
- **Biên dịch mã nguồn TypeScript sang JavaScript (thư mục `dist`):**
|
|
313
|
+
```bash
|
|
314
|
+
npm run build
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
- **Đóng gói và phát hành lên NPM registry:**
|
|
318
|
+
```bash
|
|
319
|
+
npm publish
|
|
320
|
+
```
|
|
321
|
+
*(Lưu ý: Script `prepublishOnly` trong `package.json` sẽ tự động kích hoạt tiến trình `build` trước khi publish để đảm bảo mã nguồn phát hành luôn mới nhất).*
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BloomBenchSequentialPayload, BloomCreatePayload, BloomDataPayload, DriverRequestOptions } from './types';
|
|
2
|
+
import { DriverValidator } from './DriverValidator';
|
|
3
|
+
import { HttpClient } from './HttpClient';
|
|
4
|
+
export declare class BloomApiClient {
|
|
5
|
+
private readonly http;
|
|
6
|
+
private readonly validator;
|
|
7
|
+
constructor(http: HttpClient, validator: DriverValidator);
|
|
8
|
+
add(payload: BloomDataPayload, options?: DriverRequestOptions): Promise<void>;
|
|
9
|
+
create(payload: BloomCreatePayload, options?: DriverRequestOptions): Promise<void>;
|
|
10
|
+
benchLoadSequentialKeys(payload: BloomBenchSequentialPayload, options?: DriverRequestOptions): Promise<void>;
|
|
11
|
+
mightContain(payload: BloomDataPayload, options?: DriverRequestOptions): Promise<boolean>;
|
|
12
|
+
remove(payload: BloomDataPayload, options?: DriverRequestOptions): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BloomApiClient = void 0;
|
|
4
|
+
class BloomApiClient {
|
|
5
|
+
http;
|
|
6
|
+
validator;
|
|
7
|
+
constructor(http, validator) {
|
|
8
|
+
this.http = http;
|
|
9
|
+
this.validator = validator;
|
|
10
|
+
}
|
|
11
|
+
async add(payload, options) {
|
|
12
|
+
await this.http.post('/bloom/add', {
|
|
13
|
+
bfName: this.validator.bfName(payload.bfName),
|
|
14
|
+
data: this.validator.nonEmpty(payload.data, 'data')
|
|
15
|
+
}, options);
|
|
16
|
+
}
|
|
17
|
+
async create(payload, options) {
|
|
18
|
+
const bloomType = this.validator.bloomType(payload.bloomType);
|
|
19
|
+
const requestBody = {
|
|
20
|
+
bfName: this.validator.bfName(payload.bfName),
|
|
21
|
+
expectedValues: this.validator.positiveInt(payload.expectedValues, 'expectedValues'),
|
|
22
|
+
bloomType
|
|
23
|
+
};
|
|
24
|
+
if (payload.falsePositiveProbability !== undefined) {
|
|
25
|
+
requestBody.falsePositiveProbability = this.validator.probability(payload.falsePositiveProbability, 'falsePositiveProbability');
|
|
26
|
+
}
|
|
27
|
+
if (payload.counterBits !== undefined) {
|
|
28
|
+
requestBody.counterBits = this.validator.counterBits(payload.counterBits, bloomType);
|
|
29
|
+
}
|
|
30
|
+
await this.http.post('/bloom/create', requestBody, options);
|
|
31
|
+
}
|
|
32
|
+
async benchLoadSequentialKeys(payload, options) {
|
|
33
|
+
const requestBody = {
|
|
34
|
+
bfName: this.validator.bfName(payload.bfName),
|
|
35
|
+
to: this.validator.positiveInt(payload.to, 'to')
|
|
36
|
+
};
|
|
37
|
+
if (payload.from !== undefined) {
|
|
38
|
+
requestBody.from = this.validator.positiveInt(payload.from, 'from');
|
|
39
|
+
}
|
|
40
|
+
if (payload.keyWidth !== undefined) {
|
|
41
|
+
requestBody.keyWidth = this.validator.positiveInt(payload.keyWidth, 'keyWidth');
|
|
42
|
+
}
|
|
43
|
+
await this.http.post('/bloom/bench/load-sequential-keys', requestBody, options);
|
|
44
|
+
}
|
|
45
|
+
async mightContain(payload, options) {
|
|
46
|
+
const response = await this.http.get('/bloom/might-contain', {
|
|
47
|
+
bfName: this.validator.bfName(payload.bfName),
|
|
48
|
+
data: this.validator.nonEmpty(payload.data, 'data')
|
|
49
|
+
}, options);
|
|
50
|
+
return response === 'true';
|
|
51
|
+
}
|
|
52
|
+
async remove(payload, options) {
|
|
53
|
+
await this.http.post('/bloom/remove', {
|
|
54
|
+
bfName: this.validator.bfName(payload.bfName),
|
|
55
|
+
data: this.validator.nonEmpty(payload.data, 'data')
|
|
56
|
+
}, options);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.BloomApiClient = BloomApiClient;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { BloomBenchSequentialPayload, BloomCreatePayload, BloomDataPayload, BloomDriverConfig, DriverRequestOptions, PointQueryPayload, RangeAggregatePayload, RangeBulkAddPayload, RangeCreatePayload, RangePayload, RangeQueryPayload } from './types';
|
|
2
|
+
export declare class BloomFilterDriver {
|
|
3
|
+
private readonly bloomApi;
|
|
4
|
+
private readonly rangeApi;
|
|
5
|
+
/**
|
|
6
|
+
* Create a driver instance for talking to the Bloom Filter process service.
|
|
7
|
+
*
|
|
8
|
+
* @param config Driver configuration (base URL, defaults, timeouts, optional fetch implementation)
|
|
9
|
+
*/
|
|
10
|
+
constructor(config: BloomDriverConfig);
|
|
11
|
+
/**
|
|
12
|
+
* Add a value to a Bloom Filter.
|
|
13
|
+
*
|
|
14
|
+
* @param payload Target bloom filter name and data to add
|
|
15
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
16
|
+
* @throws {BloomDriverError} If inputs are invalid or the process request fails.
|
|
17
|
+
*/
|
|
18
|
+
add(payload: BloomDataPayload, options?: DriverRequestOptions): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Create a bloom filter namespace before first use.
|
|
21
|
+
*
|
|
22
|
+
* @param payload Target bloom filter name and expected number of values
|
|
23
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
24
|
+
* @throws {BloomDriverError} If inputs are invalid or the process request fails.
|
|
25
|
+
*/
|
|
26
|
+
createBloom(payload: BloomCreatePayload, options?: DriverRequestOptions): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Backward-compatible alias for createBloom.
|
|
29
|
+
* Prefer calling createBloom in new code.
|
|
30
|
+
*/
|
|
31
|
+
configureBloom(payload: BloomCreatePayload, options?: DriverRequestOptions): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Bulk-load keys `k-{zeroPadded(i)}` for i in [from, to] via benchmark API (single HTTP, one persist).
|
|
34
|
+
*/
|
|
35
|
+
benchLoadSequentialKeys(payload: BloomBenchSequentialPayload, options?: DriverRequestOptions): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Check whether a value might be contained in the Bloom Filter.
|
|
38
|
+
*
|
|
39
|
+
* Note: Bloom filters can return false positives. `false` means "definitely not present".
|
|
40
|
+
*
|
|
41
|
+
* @param payload Target bloom filter name and data to check
|
|
42
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
43
|
+
* @returns `true` if the process returns "true"; otherwise `false`.
|
|
44
|
+
* @throws {BloomDriverError} If inputs are invalid or the process request fails.
|
|
45
|
+
*/
|
|
46
|
+
mightContain(payload: BloomDataPayload, options?: DriverRequestOptions): Promise<boolean>;
|
|
47
|
+
/**
|
|
48
|
+
* Remove a value from a (counting) Bloom Filter.
|
|
49
|
+
*
|
|
50
|
+
* @param payload Target bloom filter name and data to remove
|
|
51
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
52
|
+
* @throws {BloomDriverError} If inputs are invalid or the process request fails.
|
|
53
|
+
*/
|
|
54
|
+
remove(payload: BloomDataPayload, options?: DriverRequestOptions): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Add a key into the RangeLH structure.
|
|
57
|
+
*
|
|
58
|
+
* @param payload Target bloom filter name and key to add
|
|
59
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
60
|
+
* @throws {BloomDriverError} If inputs are invalid or the process request fails.
|
|
61
|
+
*/
|
|
62
|
+
addRange(payload: RangePayload, options?: DriverRequestOptions): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Create a RangeLH namespace before first use.
|
|
65
|
+
*/
|
|
66
|
+
createRange(payload: RangeCreatePayload, options?: DriverRequestOptions): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Add many keys into RangeLH with a single request.
|
|
69
|
+
*/
|
|
70
|
+
addRangeBulk(payload: RangeBulkAddPayload, options?: DriverRequestOptions): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Remove a key from the RangeLH structure.
|
|
73
|
+
*
|
|
74
|
+
* @param payload Target bloom filter name and key to remove
|
|
75
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
76
|
+
* @throws {BloomDriverError} If inputs are invalid or the process request fails.
|
|
77
|
+
*/
|
|
78
|
+
removeRange(payload: RangePayload, options?: DriverRequestOptions): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Run a point query over the RangeLH structure.
|
|
81
|
+
*
|
|
82
|
+
* @param payload Target bloom filter name and query key
|
|
83
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
84
|
+
* @returns List of keys returned by the process (may be empty).
|
|
85
|
+
* @throws {BloomDriverError} If inputs are invalid, response parsing fails, or the process request fails.
|
|
86
|
+
*/
|
|
87
|
+
pointQuery(payload: PointQueryPayload, options?: DriverRequestOptions): Promise<string[]>;
|
|
88
|
+
/**
|
|
89
|
+
* Run a range query over the RangeLH structure.
|
|
90
|
+
*
|
|
91
|
+
* @param payload Target bloom filter name and range bounds (inclusive/exclusive depends on the process)
|
|
92
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
93
|
+
* @returns List of keys returned by the process (may be empty).
|
|
94
|
+
* @throws {BloomDriverError} If inputs are invalid, response parsing fails, or the process request fails.
|
|
95
|
+
*/
|
|
96
|
+
rangeQuery(payload: RangeQueryPayload, options?: DriverRequestOptions): Promise<string[]>;
|
|
97
|
+
/**
|
|
98
|
+
* Run aggregate query over a key range.
|
|
99
|
+
*
|
|
100
|
+
* @param payload Target bloom filter name, range bounds, and aggregate operation.
|
|
101
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
102
|
+
* @returns Numeric aggregate result from process.
|
|
103
|
+
* @throws {BloomDriverError} If inputs are invalid, response parsing fails, or the process request fails.
|
|
104
|
+
*/
|
|
105
|
+
aggregate(payload: RangeAggregatePayload, options?: DriverRequestOptions): Promise<number>;
|
|
106
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BloomFilterDriver = void 0;
|
|
4
|
+
const BloomApiClient_1 = require("./BloomApiClient");
|
|
5
|
+
const HttpClient_1 = require("./HttpClient");
|
|
6
|
+
const RangeApiClient_1 = require("./RangeApiClient");
|
|
7
|
+
const DriverValidator_1 = require("./DriverValidator");
|
|
8
|
+
class BloomFilterDriver {
|
|
9
|
+
bloomApi;
|
|
10
|
+
rangeApi;
|
|
11
|
+
/**
|
|
12
|
+
* Create a driver instance for talking to the Bloom Filter process service.
|
|
13
|
+
*
|
|
14
|
+
* @param config Driver configuration (base URL, defaults, timeouts, optional fetch implementation)
|
|
15
|
+
*/
|
|
16
|
+
constructor(config) {
|
|
17
|
+
const httpClient = new HttpClient_1.HttpClient(config);
|
|
18
|
+
const validator = new DriverValidator_1.DriverValidator();
|
|
19
|
+
this.bloomApi = new BloomApiClient_1.BloomApiClient(httpClient, validator);
|
|
20
|
+
this.rangeApi = new RangeApiClient_1.RangeApiClient(httpClient, validator);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Add a value to a Bloom Filter.
|
|
24
|
+
*
|
|
25
|
+
* @param payload Target bloom filter name and data to add
|
|
26
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
27
|
+
* @throws {BloomDriverError} If inputs are invalid or the process request fails.
|
|
28
|
+
*/
|
|
29
|
+
async add(payload, options) {
|
|
30
|
+
await this.bloomApi.add(payload, options);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a bloom filter namespace before first use.
|
|
34
|
+
*
|
|
35
|
+
* @param payload Target bloom filter name and expected number of values
|
|
36
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
37
|
+
* @throws {BloomDriverError} If inputs are invalid or the process request fails.
|
|
38
|
+
*/
|
|
39
|
+
async createBloom(payload, options) {
|
|
40
|
+
await this.bloomApi.create(payload, options);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Backward-compatible alias for createBloom.
|
|
44
|
+
* Prefer calling createBloom in new code.
|
|
45
|
+
*/
|
|
46
|
+
async configureBloom(payload, options) {
|
|
47
|
+
await this.createBloom(payload, options);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Bulk-load keys `k-{zeroPadded(i)}` for i in [from, to] via benchmark API (single HTTP, one persist).
|
|
51
|
+
*/
|
|
52
|
+
async benchLoadSequentialKeys(payload, options) {
|
|
53
|
+
await this.bloomApi.benchLoadSequentialKeys(payload, options);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Check whether a value might be contained in the Bloom Filter.
|
|
57
|
+
*
|
|
58
|
+
* Note: Bloom filters can return false positives. `false` means "definitely not present".
|
|
59
|
+
*
|
|
60
|
+
* @param payload Target bloom filter name and data to check
|
|
61
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
62
|
+
* @returns `true` if the process returns "true"; otherwise `false`.
|
|
63
|
+
* @throws {BloomDriverError} If inputs are invalid or the process request fails.
|
|
64
|
+
*/
|
|
65
|
+
async mightContain(payload, options) {
|
|
66
|
+
return this.bloomApi.mightContain(payload, options);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Remove a value from a (counting) Bloom Filter.
|
|
70
|
+
*
|
|
71
|
+
* @param payload Target bloom filter name and data to remove
|
|
72
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
73
|
+
* @throws {BloomDriverError} If inputs are invalid or the process request fails.
|
|
74
|
+
*/
|
|
75
|
+
async remove(payload, options) {
|
|
76
|
+
await this.bloomApi.remove(payload, options);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Add a key into the RangeLH structure.
|
|
80
|
+
*
|
|
81
|
+
* @param payload Target bloom filter name and key to add
|
|
82
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
83
|
+
* @throws {BloomDriverError} If inputs are invalid or the process request fails.
|
|
84
|
+
*/
|
|
85
|
+
async addRange(payload, options) {
|
|
86
|
+
await this.rangeApi.addRange(payload, options);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create a RangeLH namespace before first use.
|
|
90
|
+
*/
|
|
91
|
+
async createRange(payload, options) {
|
|
92
|
+
await this.rangeApi.createRange(payload, options);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Add many keys into RangeLH with a single request.
|
|
96
|
+
*/
|
|
97
|
+
async addRangeBulk(payload, options) {
|
|
98
|
+
await this.rangeApi.addRangeBulk(payload, options);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Remove a key from the RangeLH structure.
|
|
102
|
+
*
|
|
103
|
+
* @param payload Target bloom filter name and key to remove
|
|
104
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
105
|
+
* @throws {BloomDriverError} If inputs are invalid or the process request fails.
|
|
106
|
+
*/
|
|
107
|
+
async removeRange(payload, options) {
|
|
108
|
+
await this.rangeApi.removeRange(payload, options);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Run a point query over the RangeLH structure.
|
|
112
|
+
*
|
|
113
|
+
* @param payload Target bloom filter name and query key
|
|
114
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
115
|
+
* @returns List of keys returned by the process (may be empty).
|
|
116
|
+
* @throws {BloomDriverError} If inputs are invalid, response parsing fails, or the process request fails.
|
|
117
|
+
*/
|
|
118
|
+
async pointQuery(payload, options) {
|
|
119
|
+
return this.rangeApi.pointQuery(payload, options);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Run a range query over the RangeLH structure.
|
|
123
|
+
*
|
|
124
|
+
* @param payload Target bloom filter name and range bounds (inclusive/exclusive depends on the process)
|
|
125
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
126
|
+
* @returns List of keys returned by the process (may be empty).
|
|
127
|
+
* @throws {BloomDriverError} If inputs are invalid, response parsing fails, or the process request fails.
|
|
128
|
+
*/
|
|
129
|
+
async rangeQuery(payload, options) {
|
|
130
|
+
return this.rangeApi.rangeQuery(payload, options);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Run aggregate query over a key range.
|
|
134
|
+
*
|
|
135
|
+
* @param payload Target bloom filter name, range bounds, and aggregate operation.
|
|
136
|
+
* @param options Optional request options (supports cancellation via `options.signal`)
|
|
137
|
+
* @returns Numeric aggregate result from process.
|
|
138
|
+
* @throws {BloomDriverError} If inputs are invalid, response parsing fails, or the process request fails.
|
|
139
|
+
*/
|
|
140
|
+
async aggregate(payload, options) {
|
|
141
|
+
return this.rangeApi.aggregate(payload, options);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
exports.BloomFilterDriver = BloomFilterDriver;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BloomType, RangeAggregateOp, RangeConfigPayload } from './types';
|
|
2
|
+
export declare class DriverValidator {
|
|
3
|
+
bfName(value: string): string;
|
|
4
|
+
nonEmpty(value: string | undefined, fieldName: string): string;
|
|
5
|
+
positiveInt(value: number, fieldName: string): number;
|
|
6
|
+
bloomType(value: BloomType | undefined): BloomType;
|
|
7
|
+
counterBits(value: number, bloomType: BloomType): number;
|
|
8
|
+
probability(value: number, fieldName: string): number;
|
|
9
|
+
aggregateOp(value: RangeAggregateOp): RangeAggregateOp;
|
|
10
|
+
parseStringList(body: string): string[];
|
|
11
|
+
parseNumericValue(body: string): number;
|
|
12
|
+
positiveNumberInZeroOne(value: number, fieldName: string): number;
|
|
13
|
+
keysArray(values: string[]): string[];
|
|
14
|
+
rangeConfig(config: RangeConfigPayload): RangeConfigPayload;
|
|
15
|
+
integerInRange(value: number, fieldName: string, min: number, max: number): number;
|
|
16
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DriverValidator = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
class DriverValidator {
|
|
6
|
+
bfName(value) {
|
|
7
|
+
return this.nonEmpty(value, 'bfName');
|
|
8
|
+
}
|
|
9
|
+
nonEmpty(value, fieldName) {
|
|
10
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
11
|
+
throw new errors_1.BloomDriverError(`"${fieldName}" must be a non-empty string`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
positiveInt(value, fieldName) {
|
|
16
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
17
|
+
throw new errors_1.BloomDriverError(`"${fieldName}" must be a positive integer`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
bloomType(value) {
|
|
22
|
+
if (value === undefined) {
|
|
23
|
+
return 'standard';
|
|
24
|
+
}
|
|
25
|
+
if (value !== 'standard' && value !== 'counting') {
|
|
26
|
+
throw new errors_1.BloomDriverError('"bloomType" must be either "standard" or "counting"');
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
counterBits(value, bloomType) {
|
|
31
|
+
if (bloomType !== 'counting') {
|
|
32
|
+
throw new errors_1.BloomDriverError('"counterBits" is only valid when bloomType is "counting"');
|
|
33
|
+
}
|
|
34
|
+
if (!Number.isInteger(value) || value < 1 || value > 8) {
|
|
35
|
+
throw new errors_1.BloomDriverError('"counterBits" must be an integer in [1, 8]');
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
probability(value, fieldName) {
|
|
40
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value >= 1) {
|
|
41
|
+
throw new errors_1.BloomDriverError(`"${fieldName}" must be a number in (0, 1)`);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
aggregateOp(value) {
|
|
46
|
+
if (value !== 'sum' && value !== 'avg' && value !== 'min' && value !== 'max' && value !== 'count') {
|
|
47
|
+
throw new errors_1.BloomDriverError('"op" must be one of: sum, avg, min, max, count');
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
parseStringList(body) {
|
|
52
|
+
if (body.length === 0) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
return body
|
|
56
|
+
.split(',')
|
|
57
|
+
.map((value) => value.trim())
|
|
58
|
+
.filter((value) => value.length > 0);
|
|
59
|
+
}
|
|
60
|
+
parseNumericValue(body) {
|
|
61
|
+
const trimmed = body.trim();
|
|
62
|
+
const value = Number.parseFloat(trimmed);
|
|
63
|
+
if (!Number.isFinite(value)) {
|
|
64
|
+
throw new errors_1.BloomDriverError(`Unable to parse numeric value "${trimmed}" from process response`);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
positiveNumberInZeroOne(value, fieldName) {
|
|
69
|
+
return this.probability(value, fieldName);
|
|
70
|
+
}
|
|
71
|
+
keysArray(values) {
|
|
72
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
73
|
+
throw new errors_1.BloomDriverError('"keys" must be a non-empty array');
|
|
74
|
+
}
|
|
75
|
+
return values.map((value, index) => this.nonEmpty(value, `keys[${index}]`));
|
|
76
|
+
}
|
|
77
|
+
rangeConfig(config) {
|
|
78
|
+
return {
|
|
79
|
+
masterInitNumBucket: this.positiveInt(config.masterInitNumBucket, 'masterInitNumBucket'),
|
|
80
|
+
masterSplitPolicy: this.probability(config.masterSplitPolicy, 'masterSplitPolicy'),
|
|
81
|
+
expectedNItems: this.positiveInt(config.expectedNItems, 'expectedNItems'),
|
|
82
|
+
fpProbBloomRF: this.probability(config.fpProbBloomRF, 'fpProbBloomRF'),
|
|
83
|
+
deltaBloomRF: this.integerInRange(config.deltaBloomRF, 'deltaBloomRF', 1, 31),
|
|
84
|
+
keyLength: this.integerInRange(config.keyLength, 'keyLength', 1, 64),
|
|
85
|
+
maxBytesString: this.integerInRange(config.maxBytesString, 'maxBytesString', 1, 8),
|
|
86
|
+
floatScale: this.positiveInt(config.floatScale, 'floatScale')
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
integerInRange(value, fieldName, min, max) {
|
|
90
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
91
|
+
throw new errors_1.BloomDriverError(`"${fieldName}" must be an integer in [${min}, ${max}]`);
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.DriverValidator = DriverValidator;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BloomDriverConfig, DriverRequestOptions } from './types';
|
|
2
|
+
export declare class HttpClient {
|
|
3
|
+
private readonly baseUrl;
|
|
4
|
+
private readonly timeoutMs;
|
|
5
|
+
private readonly fetchImpl;
|
|
6
|
+
constructor(config: BloomDriverConfig);
|
|
7
|
+
get(path: string, query: Record<string, string>, options?: DriverRequestOptions): Promise<string>;
|
|
8
|
+
post(path: string, body: unknown, options?: DriverRequestOptions): Promise<string>;
|
|
9
|
+
private request;
|
|
10
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HttpClient = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
6
|
+
class HttpClient {
|
|
7
|
+
baseUrl;
|
|
8
|
+
timeoutMs;
|
|
9
|
+
fetchImpl;
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, '');
|
|
12
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
13
|
+
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
14
|
+
}
|
|
15
|
+
async get(path, query, options) {
|
|
16
|
+
const searchParams = new URLSearchParams(query);
|
|
17
|
+
return this.request(`${path}?${searchParams.toString()}`, {
|
|
18
|
+
method: 'GET',
|
|
19
|
+
signal: options?.signal
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
async post(path, body, options) {
|
|
23
|
+
return this.request(path, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json'
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify(body),
|
|
29
|
+
signal: options?.signal
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async request(path, init) {
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
35
|
+
if (init.signal) {
|
|
36
|
+
init.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
40
|
+
...init,
|
|
41
|
+
signal: controller.signal
|
|
42
|
+
});
|
|
43
|
+
const body = (await response.text()).trim();
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new errors_1.BloomDriverError(`Process request failed with status ${response.status}`, {
|
|
46
|
+
status: response.status,
|
|
47
|
+
responseBody: body
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return body;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof errors_1.BloomDriverError) {
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
57
|
+
throw new errors_1.BloomDriverError(`Process request timed out after ${this.timeoutMs}ms`);
|
|
58
|
+
}
|
|
59
|
+
throw new errors_1.BloomDriverError(error instanceof Error ? error.message : 'Unknown process request error');
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.HttpClient = HttpClient;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DriverRequestOptions, PointQueryPayload, RangeAggregatePayload, RangeBulkAddPayload, RangeCreatePayload, RangePayload, RangeQueryPayload } from './types';
|
|
2
|
+
import { DriverValidator } from './DriverValidator';
|
|
3
|
+
import { HttpClient } from './HttpClient';
|
|
4
|
+
export declare class RangeApiClient {
|
|
5
|
+
private readonly http;
|
|
6
|
+
private readonly validator;
|
|
7
|
+
constructor(http: HttpClient, validator: DriverValidator);
|
|
8
|
+
createRange(payload: RangeCreatePayload, options?: DriverRequestOptions): Promise<void>;
|
|
9
|
+
addRange(payload: RangePayload, options?: DriverRequestOptions): Promise<void>;
|
|
10
|
+
addRangeBulk(payload: RangeBulkAddPayload, options?: DriverRequestOptions): Promise<void>;
|
|
11
|
+
removeRange(payload: RangePayload, options?: DriverRequestOptions): Promise<void>;
|
|
12
|
+
pointQuery(payload: PointQueryPayload, options?: DriverRequestOptions): Promise<string[]>;
|
|
13
|
+
rangeQuery(payload: RangeQueryPayload, options?: DriverRequestOptions): Promise<string[]>;
|
|
14
|
+
aggregate(payload: RangeAggregatePayload, options?: DriverRequestOptions): Promise<number>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RangeApiClient = void 0;
|
|
4
|
+
class RangeApiClient {
|
|
5
|
+
http;
|
|
6
|
+
validator;
|
|
7
|
+
constructor(http, validator) {
|
|
8
|
+
this.http = http;
|
|
9
|
+
this.validator = validator;
|
|
10
|
+
}
|
|
11
|
+
async createRange(payload, options) {
|
|
12
|
+
const config = this.validator.rangeConfig(payload.config);
|
|
13
|
+
await this.http.post('/rangelh/create', {
|
|
14
|
+
bfName: this.validator.bfName(payload.bfName),
|
|
15
|
+
masterInitNumBucket: config.masterInitNumBucket,
|
|
16
|
+
masterSplitPolicy: config.masterSplitPolicy,
|
|
17
|
+
expectedNItems: config.expectedNItems,
|
|
18
|
+
fpProbBloomRF: config.fpProbBloomRF,
|
|
19
|
+
deltaBloomRF: config.deltaBloomRF,
|
|
20
|
+
keyLength: config.keyLength,
|
|
21
|
+
maxBytesString: config.maxBytesString,
|
|
22
|
+
floatScale: config.floatScale
|
|
23
|
+
}, options);
|
|
24
|
+
}
|
|
25
|
+
async addRange(payload, options) {
|
|
26
|
+
await this.http.post('/rangelh/add', {
|
|
27
|
+
bfName: this.validator.bfName(payload.bfName),
|
|
28
|
+
key: this.validator.nonEmpty(payload.key, 'key')
|
|
29
|
+
}, options);
|
|
30
|
+
}
|
|
31
|
+
async addRangeBulk(payload, options) {
|
|
32
|
+
await this.http.post('/rangelh/add-bulk', {
|
|
33
|
+
bfName: this.validator.bfName(payload.bfName),
|
|
34
|
+
keys: this.validator.keysArray(payload.keys)
|
|
35
|
+
}, options);
|
|
36
|
+
}
|
|
37
|
+
async removeRange(payload, options) {
|
|
38
|
+
await this.http.post('/rangelh/remove', {
|
|
39
|
+
bfName: this.validator.bfName(payload.bfName),
|
|
40
|
+
key: this.validator.nonEmpty(payload.key, 'key')
|
|
41
|
+
}, options);
|
|
42
|
+
}
|
|
43
|
+
async pointQuery(payload, options) {
|
|
44
|
+
const response = await this.http.get('/rangelh/point-query', {
|
|
45
|
+
bfName: this.validator.bfName(payload.bfName),
|
|
46
|
+
key: this.validator.nonEmpty(payload.key, 'key')
|
|
47
|
+
}, options);
|
|
48
|
+
return this.validator.parseStringList(response);
|
|
49
|
+
}
|
|
50
|
+
async rangeQuery(payload, options) {
|
|
51
|
+
const response = await this.http.get('/rangelh/range-query', {
|
|
52
|
+
bfName: this.validator.bfName(payload.bfName),
|
|
53
|
+
from: this.validator.nonEmpty(payload.from, 'from'),
|
|
54
|
+
to: this.validator.nonEmpty(payload.to, 'to')
|
|
55
|
+
}, options);
|
|
56
|
+
return this.validator.parseStringList(response);
|
|
57
|
+
}
|
|
58
|
+
async aggregate(payload, options) {
|
|
59
|
+
const response = await this.http.get('/rangelh/aggregate', {
|
|
60
|
+
bfName: this.validator.bfName(payload.bfName),
|
|
61
|
+
from: this.validator.nonEmpty(payload.from, 'from'),
|
|
62
|
+
to: this.validator.nonEmpty(payload.to, 'to'),
|
|
63
|
+
op: this.validator.aggregateOp(payload.op)
|
|
64
|
+
}, options);
|
|
65
|
+
return this.validator.parseNumericValue(response);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.RangeApiClient = RangeApiClient;
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BloomDriverError = void 0;
|
|
4
|
+
class BloomDriverError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
responseBody;
|
|
7
|
+
constructor(message, options) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'BloomDriverError';
|
|
10
|
+
this.status = options?.status;
|
|
11
|
+
this.responseBody = options?.responseBody;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.BloomDriverError = BloomDriverError;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./BloomFilterDriver"), exports);
|
|
18
|
+
__exportStar(require("./BloomApiClient"), exports);
|
|
19
|
+
__exportStar(require("./RangeApiClient"), exports);
|
|
20
|
+
__exportStar(require("./HttpClient"), exports);
|
|
21
|
+
__exportStar(require("./DriverValidator"), exports);
|
|
22
|
+
__exportStar(require("./errors"), exports);
|
|
23
|
+
__exportStar(require("./types"), exports);
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface BloomDriverConfig {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
fetchImpl?: typeof fetch;
|
|
5
|
+
}
|
|
6
|
+
export interface BloomDataPayload {
|
|
7
|
+
bfName: string;
|
|
8
|
+
data: string;
|
|
9
|
+
}
|
|
10
|
+
export type BloomType = 'standard' | 'counting';
|
|
11
|
+
export interface BloomCreatePayload {
|
|
12
|
+
bfName: string;
|
|
13
|
+
expectedValues: number;
|
|
14
|
+
bloomType?: BloomType;
|
|
15
|
+
falsePositiveProbability?: number;
|
|
16
|
+
counterBits?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Matches POST /bloom/bench/load-sequential-keys (in-process bulk insert, one save). */
|
|
19
|
+
export interface BloomBenchSequentialPayload {
|
|
20
|
+
bfName: string;
|
|
21
|
+
/** Inclusive start index; default 1 on server if omitted. */
|
|
22
|
+
from?: number;
|
|
23
|
+
/** Inclusive end index. */
|
|
24
|
+
to: number;
|
|
25
|
+
/** Zero-pad width for numeric part; default 10 on server. */
|
|
26
|
+
keyWidth?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface RangePayload {
|
|
29
|
+
bfName: string;
|
|
30
|
+
key: string;
|
|
31
|
+
}
|
|
32
|
+
export interface RangeConfigPayload {
|
|
33
|
+
masterInitNumBucket: number;
|
|
34
|
+
masterSplitPolicy: number;
|
|
35
|
+
expectedNItems: number;
|
|
36
|
+
fpProbBloomRF: number;
|
|
37
|
+
deltaBloomRF: number;
|
|
38
|
+
keyLength: number;
|
|
39
|
+
maxBytesString: number;
|
|
40
|
+
floatScale: number;
|
|
41
|
+
}
|
|
42
|
+
export interface RangeCreatePayload {
|
|
43
|
+
bfName: string;
|
|
44
|
+
config: RangeConfigPayload;
|
|
45
|
+
}
|
|
46
|
+
export interface RangeBulkAddPayload {
|
|
47
|
+
bfName: string;
|
|
48
|
+
keys: string[];
|
|
49
|
+
}
|
|
50
|
+
export interface PointQueryPayload {
|
|
51
|
+
bfName: string;
|
|
52
|
+
key: string;
|
|
53
|
+
}
|
|
54
|
+
export interface RangeQueryPayload {
|
|
55
|
+
bfName: string;
|
|
56
|
+
from: string;
|
|
57
|
+
to: string;
|
|
58
|
+
}
|
|
59
|
+
export type RangeAggregateOp = 'sum' | 'avg' | 'min' | 'max' | 'count';
|
|
60
|
+
export interface RangeAggregatePayload {
|
|
61
|
+
bfName: string;
|
|
62
|
+
from: string;
|
|
63
|
+
to: string;
|
|
64
|
+
op: RangeAggregateOp;
|
|
65
|
+
}
|
|
66
|
+
export interface DriverRequestOptions {
|
|
67
|
+
signal?: AbortSignal;
|
|
68
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bloom-filter-driver",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "HTTP driver for BloomFilterProcess",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "rimraf ./dist && tsc",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"bloom-filter",
|
|
16
|
+
"driver",
|
|
17
|
+
"http-client"
|
|
18
|
+
],
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/jest": "^30.0.0",
|
|
22
|
+
"@types/node": "^25.5.0",
|
|
23
|
+
"jest": "^30.3.0",
|
|
24
|
+
"rimraf": "^6.1.3",
|
|
25
|
+
"ts-jest": "^29.4.9",
|
|
26
|
+
"typescript": "~5.9.3"
|
|
27
|
+
}
|
|
28
|
+
}
|