@lv-agent/logdb-client 0.1.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 +67 -0
- package/dist/client.d.ts +64 -0
- package/dist/client.js +289 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +27 -0
- package/dist/tail-stream.d.ts +14 -0
- package/dist/tail-stream.js +50 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.js +2 -0
- package/package.json +20 -0
- package/proto/logdbd.proto +262 -0
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @lv-agent/logdb-client — TypeScript SDK for logdbd
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @lv-agent/logdb-client
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { Client } from '@lv-agent/logdb-client';
|
|
13
|
+
|
|
14
|
+
const client = new Client('127.0.0.1:50051');
|
|
15
|
+
|
|
16
|
+
// Append
|
|
17
|
+
const { seq } = await client.append('my-app', 'main', 'test.event', Buffer.from('hello'));
|
|
18
|
+
console.log('appended at seq', seq);
|
|
19
|
+
|
|
20
|
+
// Read
|
|
21
|
+
const rec = await client.read('my-app', 'main', 1);
|
|
22
|
+
console.log(rec?.eventType);
|
|
23
|
+
|
|
24
|
+
// Scan all
|
|
25
|
+
const records = await client.scanAll('my-app', 'main', 0);
|
|
26
|
+
for (const r of records) {
|
|
27
|
+
console.log(`[${r.seq}] ${r.eventType}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Tail (live subscription)
|
|
31
|
+
const stream = client.tail('my-app', 'main', { fromSeq: 0, consumerGroup: 'workers', consumerId: 'w1' });
|
|
32
|
+
for await (const rec of stream) {
|
|
33
|
+
console.log(`[${rec.seq}] ${rec.eventType}`);
|
|
34
|
+
// Commit progress
|
|
35
|
+
await client.commitOffset('my-app', 'main', 'workers', 'w1', rec.seq);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
| Method | Description |
|
|
42
|
+
|--------|-------------|
|
|
43
|
+
| `append(ns, stream, eventType, content)` | Write a record |
|
|
44
|
+
| `read(ns, stream, seq)` | Point read |
|
|
45
|
+
| `scanAll(ns, stream, fromSeq)` | Scan all records |
|
|
46
|
+
| `tail(ns, stream, opts)` | Live subscription (async iterable) |
|
|
47
|
+
| `listNamespaces()` | List all namespaces |
|
|
48
|
+
| `listStreams(ns)` | List streams in a namespace |
|
|
49
|
+
| `status()` | Node status |
|
|
50
|
+
| `verifyChain(ns, stream)` | Verify hash chain |
|
|
51
|
+
| `commitOffset(...)` / `committedOffset(...)` | Consumer group offset management |
|
|
52
|
+
|
|
53
|
+
## TLS / mTLS
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import * as fs from 'fs';
|
|
57
|
+
|
|
58
|
+
const client = new Client('logdbd.example.com:50051', {
|
|
59
|
+
tlsCa: fs.readFileSync('/etc/certs/ca.crt'),
|
|
60
|
+
tlsCert: fs.readFileSync('/etc/certs/client.crt'), // mTLS
|
|
61
|
+
tlsKey: fs.readFileSync('/etc/certs/client.key'), // mTLS
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
Apache-2.0
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { TailStream } from './tail-stream';
|
|
2
|
+
import type { AppendResult, BatchAppendResult, NamespaceInfo, LogRecord, StreamInfo, StatusResult, Watermark, VerifyResult, TailOptions } from './types';
|
|
3
|
+
export interface ClientOptions {
|
|
4
|
+
/** TLS certificate authority (PEM). */
|
|
5
|
+
tlsCa?: Buffer;
|
|
6
|
+
/** TLS client cert (PEM) for mTLS. */
|
|
7
|
+
tlsCert?: Buffer;
|
|
8
|
+
/** TLS client key (PEM) for mTLS. */
|
|
9
|
+
tlsKey?: Buffer;
|
|
10
|
+
/** Bearer auth token. */
|
|
11
|
+
authToken?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class Client {
|
|
14
|
+
private client;
|
|
15
|
+
private closed;
|
|
16
|
+
/**
|
|
17
|
+
* Create a new logdbd client.
|
|
18
|
+
*
|
|
19
|
+
* @param addr — host:port, e.g. "127.0.0.1:50051"
|
|
20
|
+
* @param opts — optional TLS and auth settings
|
|
21
|
+
*/
|
|
22
|
+
constructor(addr: string, opts?: ClientOptions);
|
|
23
|
+
/** Append a record. Returns the assigned seq. */
|
|
24
|
+
append(namespace: string, stream: string, eventType: string, content: Buffer): Promise<AppendResult>;
|
|
25
|
+
/** Append with full options. */
|
|
26
|
+
appendFull(opts: {
|
|
27
|
+
namespace: string;
|
|
28
|
+
stream: string;
|
|
29
|
+
eventType: string;
|
|
30
|
+
contentType?: string;
|
|
31
|
+
metadata?: Record<string, string>;
|
|
32
|
+
timestampNs?: number;
|
|
33
|
+
content: Buffer;
|
|
34
|
+
}): Promise<AppendResult>;
|
|
35
|
+
/** Batch append — all in same namespace+stream. */
|
|
36
|
+
appendBatch(requests: Array<{
|
|
37
|
+
namespace: string;
|
|
38
|
+
stream: string;
|
|
39
|
+
eventType: string;
|
|
40
|
+
content: Buffer;
|
|
41
|
+
}>): Promise<BatchAppendResult>;
|
|
42
|
+
/** Read a record by seq. Returns null if not found. */
|
|
43
|
+
read(namespace: string, stream: string, seq: number): Promise<LogRecord | null>;
|
|
44
|
+
/** Scan records in range. Returns array of records. */
|
|
45
|
+
scanAll(namespace: string, stream: string, fromSeq: number, limit?: number): Promise<LogRecord[]>;
|
|
46
|
+
/** Subscribe to new records via Tail. */
|
|
47
|
+
tail(namespace: string, stream: string, opts?: TailOptions): TailStream;
|
|
48
|
+
/** Get the watermark for a namespace/stream. */
|
|
49
|
+
watermark(namespace: string, stream: string): Promise<Watermark>;
|
|
50
|
+
/** List all namespaces. */
|
|
51
|
+
listNamespaces(): Promise<NamespaceInfo[]>;
|
|
52
|
+
/** List streams in a namespace. */
|
|
53
|
+
listStreams(namespace: string): Promise<StreamInfo[]>;
|
|
54
|
+
/** Get node status. */
|
|
55
|
+
status(): Promise<StatusResult>;
|
|
56
|
+
/** Verify hash chain for a stream. */
|
|
57
|
+
verifyChain(namespace: string, stream: string, fromSeq?: number, toSeq?: number): Promise<VerifyResult>;
|
|
58
|
+
/** Commit consumer offset. */
|
|
59
|
+
commitOffset(namespace: string, stream: string, consumerGroup: string, consumerId: string, seq: number): Promise<void>;
|
|
60
|
+
/** Get committed offset for a consumer. */
|
|
61
|
+
committedOffset(namespace: string, stream: string, consumerGroup: string, consumerId: string): Promise<number>;
|
|
62
|
+
/** Close the client connection. */
|
|
63
|
+
close(): void;
|
|
64
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Client = void 0;
|
|
37
|
+
const grpc = __importStar(require("@grpc/grpc-js"));
|
|
38
|
+
const protoLoader = __importStar(require("@grpc/proto-loader"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const tail_stream_1 = require("./tail-stream");
|
|
41
|
+
let protoDef = null;
|
|
42
|
+
function loadProto() {
|
|
43
|
+
if (protoDef)
|
|
44
|
+
return protoDef;
|
|
45
|
+
const protoPath = path.join(__dirname, '..', 'proto', 'logdbd.proto');
|
|
46
|
+
const pkgDef = protoLoader.loadSync(protoPath, {
|
|
47
|
+
keepCase: true,
|
|
48
|
+
longs: String,
|
|
49
|
+
enums: String,
|
|
50
|
+
defaults: true,
|
|
51
|
+
oneofs: true,
|
|
52
|
+
});
|
|
53
|
+
protoDef = grpc.loadPackageDefinition(pkgDef).logdbd;
|
|
54
|
+
return protoDef;
|
|
55
|
+
}
|
|
56
|
+
class Client {
|
|
57
|
+
client;
|
|
58
|
+
closed = false;
|
|
59
|
+
/**
|
|
60
|
+
* Create a new logdbd client.
|
|
61
|
+
*
|
|
62
|
+
* @param addr — host:port, e.g. "127.0.0.1:50051"
|
|
63
|
+
* @param opts — optional TLS and auth settings
|
|
64
|
+
*/
|
|
65
|
+
constructor(addr, opts) {
|
|
66
|
+
const proto = loadProto();
|
|
67
|
+
const isTls = opts?.tlsCa || addr.startsWith('https://');
|
|
68
|
+
let creds;
|
|
69
|
+
if (isTls && opts?.tlsCert) {
|
|
70
|
+
creds = grpc.credentials.createSsl(opts.tlsCa, opts.tlsKey, opts.tlsCert);
|
|
71
|
+
}
|
|
72
|
+
else if (isTls) {
|
|
73
|
+
creds = grpc.credentials.createSsl(opts?.tlsCa);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
creds = grpc.credentials.createInsecure();
|
|
77
|
+
}
|
|
78
|
+
const target = addr.startsWith('http') ? addr : (isTls ? `https://${addr}` : `http://${addr}`);
|
|
79
|
+
// Strip protocol for grpc-js
|
|
80
|
+
const grpcTarget = target.replace(/^https?:\/\//, '');
|
|
81
|
+
this.client = new proto.LogDbService(grpcTarget, creds, {
|
|
82
|
+
'grpc.keepalive_time_ms': 30000,
|
|
83
|
+
'grpc.keepalive_timeout_ms': 10000,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// ── Write ──────────────────────────────────────────────────────────
|
|
87
|
+
/** Append a record. Returns the assigned seq. */
|
|
88
|
+
append(namespace, stream, eventType, content) {
|
|
89
|
+
return this.appendFull({
|
|
90
|
+
namespace, stream, eventType,
|
|
91
|
+
content, contentType: 'application/json',
|
|
92
|
+
metadata: {}, timestampNs: 0,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** Append with full options. */
|
|
96
|
+
appendFull(opts) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
this.client.Append({
|
|
99
|
+
namespace: opts.namespace,
|
|
100
|
+
stream: opts.stream,
|
|
101
|
+
eventType: opts.eventType,
|
|
102
|
+
content: opts.content,
|
|
103
|
+
contentType: opts.contentType || 'application/json',
|
|
104
|
+
metadata: (opts.metadata || {}),
|
|
105
|
+
timestampNs: opts.timestampNs || 0,
|
|
106
|
+
}, (err, resp) => {
|
|
107
|
+
if (err)
|
|
108
|
+
return reject(err);
|
|
109
|
+
resolve({
|
|
110
|
+
namespaceId: resp.namespaceId,
|
|
111
|
+
streamId: resp.streamId,
|
|
112
|
+
seq: resp.seq,
|
|
113
|
+
gid: resp.gid,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/** Batch append — all in same namespace+stream. */
|
|
119
|
+
appendBatch(requests) {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
this.client.BatchAppend({ requests }, (err, resp) => {
|
|
122
|
+
if (err)
|
|
123
|
+
return reject(err);
|
|
124
|
+
resolve({
|
|
125
|
+
records: (resp.records || []).map((r) => ({
|
|
126
|
+
namespaceId: r.namespaceId,
|
|
127
|
+
streamId: r.streamId,
|
|
128
|
+
seq: r.seq,
|
|
129
|
+
gid: r.gid,
|
|
130
|
+
})),
|
|
131
|
+
error: resp.error ? { code: resp.error.code, message: resp.error.message } : undefined,
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// ── Read ───────────────────────────────────────────────────────────
|
|
137
|
+
/** Read a record by seq. Returns null if not found. */
|
|
138
|
+
read(namespace, stream, seq) {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
this.client.Read({ namespace, stream, seq }, (err, resp) => {
|
|
141
|
+
if (err)
|
|
142
|
+
return reject(err);
|
|
143
|
+
if (!resp.found || !resp.record)
|
|
144
|
+
return resolve(null);
|
|
145
|
+
resolve(convertRecord(resp.record));
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/** Scan records in range. Returns array of records. */
|
|
150
|
+
scanAll(namespace, stream, fromSeq, limit = 10000) {
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
const call = this.client.Scan({ namespace, stream, fromSeq, toSeq: 0, limit });
|
|
153
|
+
const records = [];
|
|
154
|
+
call.on('data', (resp) => {
|
|
155
|
+
for (const r of resp.records || [])
|
|
156
|
+
records.push(convertRecord(r));
|
|
157
|
+
});
|
|
158
|
+
call.on('end', () => resolve(records));
|
|
159
|
+
call.on('error', (err) => reject(err));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/** Subscribe to new records via Tail. */
|
|
163
|
+
tail(namespace, stream, opts) {
|
|
164
|
+
const call = this.client.Tail({
|
|
165
|
+
namespace, stream,
|
|
166
|
+
fromSeq: opts?.fromSeq ?? 0,
|
|
167
|
+
batchSize: opts?.batchSize ?? 100,
|
|
168
|
+
consumerGroup: opts?.consumerGroup ?? '',
|
|
169
|
+
consumerId: opts?.consumerId ?? '',
|
|
170
|
+
});
|
|
171
|
+
return new tail_stream_1.TailStream(call, convertRecord);
|
|
172
|
+
}
|
|
173
|
+
// ── Watermark ──────────────────────────────────────────────────────
|
|
174
|
+
/** Get the watermark for a namespace/stream. */
|
|
175
|
+
watermark(namespace, stream) {
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
this.client.GetWatermark({ namespace, stream }, (err, resp) => {
|
|
178
|
+
if (err)
|
|
179
|
+
return reject(err);
|
|
180
|
+
resolve({
|
|
181
|
+
namespace: resp.namespace,
|
|
182
|
+
stream: resp.stream,
|
|
183
|
+
oldestSeq: Number(resp.oldestSeq),
|
|
184
|
+
durableSeq: Number(resp.durableSeq),
|
|
185
|
+
replicatedSeq: Number(resp.replicatedSeq),
|
|
186
|
+
nodeId: resp.nodeId,
|
|
187
|
+
role: resp.role,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// ── Admin ──────────────────────────────────────────────────────────
|
|
193
|
+
/** List all namespaces. */
|
|
194
|
+
listNamespaces() {
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
this.client.ListNamespaces({}, (err, resp) => {
|
|
197
|
+
if (err)
|
|
198
|
+
return reject(err);
|
|
199
|
+
resolve((resp.namespaces || []).map((n) => ({
|
|
200
|
+
name: n.name, id: n.id, streamCount: Number(n.streamCount),
|
|
201
|
+
})));
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/** List streams in a namespace. */
|
|
206
|
+
listStreams(namespace) {
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
this.client.ListStreams({ namespace }, (err, resp) => {
|
|
209
|
+
if (err)
|
|
210
|
+
return reject(err);
|
|
211
|
+
resolve((resp.streams || []).map((s) => ({
|
|
212
|
+
name: s.name, id: Number(s.id),
|
|
213
|
+
firstSeq: Number(s.firstSeq), durableSeq: Number(s.durableSeq),
|
|
214
|
+
recordCount: Number(s.recordCount),
|
|
215
|
+
})));
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/** Get node status. */
|
|
220
|
+
status() {
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
this.client.Status({}, (err, resp) => {
|
|
223
|
+
if (err)
|
|
224
|
+
return reject(err);
|
|
225
|
+
resolve({
|
|
226
|
+
nodeId: resp.nodeId,
|
|
227
|
+
role: resp.role || '',
|
|
228
|
+
durableSequence: Number(resp.durableSequence),
|
|
229
|
+
checkpoint: Number(resp.checkpoint),
|
|
230
|
+
walBytesUsed: Number(resp.walBytesUsed),
|
|
231
|
+
walBytesTotal: Number(resp.walBytesTotal),
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/** Verify hash chain for a stream. */
|
|
237
|
+
verifyChain(namespace, stream, fromSeq = 0, toSeq = 0) {
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
this.client.VerifyChain({ namespace, stream, fromSeq, toSeq }, (err, resp) => {
|
|
240
|
+
if (err)
|
|
241
|
+
return reject(err);
|
|
242
|
+
resolve({
|
|
243
|
+
ok: resp.ok,
|
|
244
|
+
verifiedFrom: Number(resp.verifiedFrom),
|
|
245
|
+
verifiedTo: Number(resp.verifiedTo),
|
|
246
|
+
errorAtSeq: Number(resp.errorAtSeq),
|
|
247
|
+
errorMessage: resp.errorMessage,
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/** Commit consumer offset. */
|
|
253
|
+
commitOffset(namespace, stream, consumerGroup, consumerId, seq) {
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
this.client.CommitOffset({ namespace, stream, consumerGroup, consumerId, committedSeq: seq }, (err) => { if (err)
|
|
256
|
+
return reject(err); resolve(); });
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/** Get committed offset for a consumer. */
|
|
260
|
+
committedOffset(namespace, stream, consumerGroup, consumerId) {
|
|
261
|
+
return new Promise((resolve, reject) => {
|
|
262
|
+
this.client.GetCommittedOffset({ namespace, stream, consumerGroup, consumerId }, (err, resp) => {
|
|
263
|
+
if (err)
|
|
264
|
+
return reject(err);
|
|
265
|
+
resolve(Number(resp.committedSeq));
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/** Close the client connection. */
|
|
270
|
+
close() {
|
|
271
|
+
if (!this.closed) {
|
|
272
|
+
this.closed = true;
|
|
273
|
+
grpc.closeClient(this.client);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
exports.Client = Client;
|
|
278
|
+
function convertRecord(r) {
|
|
279
|
+
return {
|
|
280
|
+
namespaceId: r.namespaceId,
|
|
281
|
+
streamId: Number(r.streamId),
|
|
282
|
+
seq: Number(r.seq),
|
|
283
|
+
eventType: r.eventType,
|
|
284
|
+
timestampNs: Number(r.timestampNs),
|
|
285
|
+
contentType: r.contentType,
|
|
286
|
+
metadata: r.metadata || {},
|
|
287
|
+
content: Buffer.isBuffer(r.content) ? r.content : Buffer.from(r.content || ''),
|
|
288
|
+
};
|
|
289
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logdb-client — TypeScript SDK for logdbd
|
|
3
|
+
*
|
|
4
|
+
* ```typescript
|
|
5
|
+
* import { Client } from 'logdb-client';
|
|
6
|
+
*
|
|
7
|
+
* const client = new Client('127.0.0.1:50051');
|
|
8
|
+
*
|
|
9
|
+
* // Append
|
|
10
|
+
* const { seq } = await client.append('my-app', 'main', 'test.event', Buffer.from('hello'));
|
|
11
|
+
*
|
|
12
|
+
* // Read
|
|
13
|
+
* const rec = await client.read('my-app', 'main', 1);
|
|
14
|
+
*
|
|
15
|
+
* // Tail (live subscription)
|
|
16
|
+
* for await (const rec of client.tail('my-app', 'main', { fromSeq: 0 })) {
|
|
17
|
+
* console.log(rec.seq, rec.eventType);
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export { Client, ClientOptions } from './client';
|
|
22
|
+
export { TailStream } from './tail-stream';
|
|
23
|
+
export { LogRecord, AppendResult, Watermark, VerifyResult, TailOptions } from './types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* logdb-client — TypeScript SDK for logdbd
|
|
4
|
+
*
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { Client } from 'logdb-client';
|
|
7
|
+
*
|
|
8
|
+
* const client = new Client('127.0.0.1:50051');
|
|
9
|
+
*
|
|
10
|
+
* // Append
|
|
11
|
+
* const { seq } = await client.append('my-app', 'main', 'test.event', Buffer.from('hello'));
|
|
12
|
+
*
|
|
13
|
+
* // Read
|
|
14
|
+
* const rec = await client.read('my-app', 'main', 1);
|
|
15
|
+
*
|
|
16
|
+
* // Tail (live subscription)
|
|
17
|
+
* for await (const rec of client.tail('my-app', 'main', { fromSeq: 0 })) {
|
|
18
|
+
* console.log(rec.seq, rec.eventType);
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.TailStream = exports.Client = void 0;
|
|
24
|
+
var client_1 = require("./client");
|
|
25
|
+
Object.defineProperty(exports, "Client", { enumerable: true, get: function () { return client_1.Client; } });
|
|
26
|
+
var tail_stream_1 = require("./tail-stream");
|
|
27
|
+
Object.defineProperty(exports, "TailStream", { enumerable: true, get: function () { return tail_stream_1.TailStream; } });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LogRecord } from './types';
|
|
2
|
+
/** Async iterable stream of records from a Tail subscription. */
|
|
3
|
+
export declare class TailStream implements AsyncIterable<LogRecord> {
|
|
4
|
+
private readonly convert;
|
|
5
|
+
private call;
|
|
6
|
+
private buffer;
|
|
7
|
+
private done;
|
|
8
|
+
constructor(call: any, convert: (r: any) => LogRecord);
|
|
9
|
+
/** Get the next record (waits for new data). */
|
|
10
|
+
next(): Promise<LogRecord | null>;
|
|
11
|
+
/** Cancel the subscription. */
|
|
12
|
+
cancel(): void;
|
|
13
|
+
[Symbol.asyncIterator](): AsyncIterator<LogRecord>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TailStream = void 0;
|
|
4
|
+
/** Async iterable stream of records from a Tail subscription. */
|
|
5
|
+
class TailStream {
|
|
6
|
+
convert;
|
|
7
|
+
call = null;
|
|
8
|
+
buffer = [];
|
|
9
|
+
done = false;
|
|
10
|
+
constructor(call, convert) {
|
|
11
|
+
this.convert = convert;
|
|
12
|
+
this.call = call;
|
|
13
|
+
call.on('data', (resp) => {
|
|
14
|
+
if (resp.heartbeat)
|
|
15
|
+
return;
|
|
16
|
+
for (const r of resp.records || []) {
|
|
17
|
+
this.buffer.push(convert(r));
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
call.on('end', () => { this.done = true; });
|
|
21
|
+
call.on('error', () => { this.done = true; });
|
|
22
|
+
}
|
|
23
|
+
/** Get the next record (waits for new data). */
|
|
24
|
+
async next() {
|
|
25
|
+
while (this.buffer.length === 0 && !this.done) {
|
|
26
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
27
|
+
}
|
|
28
|
+
if (this.buffer.length > 0) {
|
|
29
|
+
return this.buffer.shift();
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/** Cancel the subscription. */
|
|
34
|
+
cancel() {
|
|
35
|
+
if (this.call) {
|
|
36
|
+
this.call.cancel();
|
|
37
|
+
this.call = null;
|
|
38
|
+
}
|
|
39
|
+
this.done = true;
|
|
40
|
+
}
|
|
41
|
+
[Symbol.asyncIterator]() {
|
|
42
|
+
return {
|
|
43
|
+
next: async () => {
|
|
44
|
+
const rec = await this.next();
|
|
45
|
+
return rec ? { value: rec, done: false } : { value: undefined, done: true };
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.TailStream = TailStream;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** A logdbd record. */
|
|
2
|
+
export interface LogRecord {
|
|
3
|
+
namespaceId: number;
|
|
4
|
+
streamId: number;
|
|
5
|
+
seq: number;
|
|
6
|
+
eventType: string;
|
|
7
|
+
timestampNs: number;
|
|
8
|
+
contentType: string;
|
|
9
|
+
metadata: Record<string, string>;
|
|
10
|
+
content: Buffer;
|
|
11
|
+
}
|
|
12
|
+
/** Response from append(). */
|
|
13
|
+
export interface AppendResult {
|
|
14
|
+
namespaceId: number;
|
|
15
|
+
streamId: number;
|
|
16
|
+
seq: number;
|
|
17
|
+
gid: number;
|
|
18
|
+
}
|
|
19
|
+
/** Batch append response. */
|
|
20
|
+
export interface BatchAppendResult {
|
|
21
|
+
records: AppendResult[];
|
|
22
|
+
error?: {
|
|
23
|
+
code: string;
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Namespace info. */
|
|
28
|
+
export interface NamespaceInfo {
|
|
29
|
+
name: string;
|
|
30
|
+
id: number;
|
|
31
|
+
streamCount: number;
|
|
32
|
+
}
|
|
33
|
+
/** Stream info. */
|
|
34
|
+
export interface StreamInfo {
|
|
35
|
+
name: string;
|
|
36
|
+
id: number;
|
|
37
|
+
firstSeq: number;
|
|
38
|
+
durableSeq: number;
|
|
39
|
+
recordCount: number;
|
|
40
|
+
}
|
|
41
|
+
/** Watermark. */
|
|
42
|
+
export interface Watermark {
|
|
43
|
+
namespace: string;
|
|
44
|
+
stream: string;
|
|
45
|
+
oldestSeq: number;
|
|
46
|
+
durableSeq: number;
|
|
47
|
+
replicatedSeq: number;
|
|
48
|
+
nodeId: string;
|
|
49
|
+
role: string;
|
|
50
|
+
}
|
|
51
|
+
/** Node status. */
|
|
52
|
+
export interface StatusResult {
|
|
53
|
+
nodeId: string;
|
|
54
|
+
role: string;
|
|
55
|
+
durableSequence: number;
|
|
56
|
+
checkpoint: number;
|
|
57
|
+
walBytesUsed: number;
|
|
58
|
+
walBytesTotal: number;
|
|
59
|
+
}
|
|
60
|
+
/** Hash chain verification result. */
|
|
61
|
+
export interface VerifyResult {
|
|
62
|
+
ok: boolean;
|
|
63
|
+
verifiedFrom: number;
|
|
64
|
+
verifiedTo: number;
|
|
65
|
+
errorAtSeq: number;
|
|
66
|
+
errorMessage: string;
|
|
67
|
+
}
|
|
68
|
+
/** Tail options. */
|
|
69
|
+
export interface TailOptions {
|
|
70
|
+
fromSeq?: number;
|
|
71
|
+
batchSize?: number;
|
|
72
|
+
consumerGroup?: string;
|
|
73
|
+
consumerId?: string;
|
|
74
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lv-agent/logdb-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for logdbd — append-only audit log database",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": ["dist", "proto"],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"prepublishOnly": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@grpc/grpc-js": "^1.12.0",
|
|
14
|
+
"@grpc/proto-loader": "^0.7.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22.0.0",
|
|
18
|
+
"typescript": "^5.6.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
package logdbd;
|
|
3
|
+
|
|
4
|
+
service LogDbService {
|
|
5
|
+
rpc Append(AppendRequest) returns (AppendResponse);
|
|
6
|
+
rpc BatchAppend(BatchAppendRequest) returns (AppendBatchResponse);
|
|
7
|
+
rpc Read(ReadRequest) returns (ReadResponse);
|
|
8
|
+
rpc Scan(ScanRequest) returns (stream ScanResponse);
|
|
9
|
+
rpc Tail(TailRequest) returns (stream TailResponse);
|
|
10
|
+
rpc Checkpoint(CheckpointRequest) returns (CheckpointResponse);
|
|
11
|
+
rpc Status(StatusRequest) returns (StatusResponse);
|
|
12
|
+
rpc GetWatermark(GetWatermarkRequest) returns (Watermark);
|
|
13
|
+
rpc VerifyChain(VerifyChainRequest) returns (VerifyChainResponse);
|
|
14
|
+
|
|
15
|
+
// Consumer group — server-side offset tracking
|
|
16
|
+
rpc CommitOffset(CommitOffsetRequest) returns (CommitOffsetResponse);
|
|
17
|
+
rpc GetCommittedOffset(GetCommittedOffsetRequest) returns (GetCommittedOffsetResponse);
|
|
18
|
+
|
|
19
|
+
// Catalog — namespace & stream management
|
|
20
|
+
rpc ListNamespaces(ListNamespacesRequest) returns (ListNamespacesResponse);
|
|
21
|
+
rpc ListStreams(ListStreamsRequest) returns (ListStreamsResponse);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Record (full logdbd record) ─────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
message Record {
|
|
27
|
+
uint32 namespace_id = 1;
|
|
28
|
+
uint64 stream_id = 2;
|
|
29
|
+
uint64 seq = 3;
|
|
30
|
+
string event_type = 4;
|
|
31
|
+
uint64 timestamp_ns = 5;
|
|
32
|
+
string content_type = 6;
|
|
33
|
+
map<string, string> metadata = 7;
|
|
34
|
+
bytes content = 8;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Write ───────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
message AppendRequest {
|
|
40
|
+
string namespace = 1;
|
|
41
|
+
string stream = 2;
|
|
42
|
+
string event_type = 3;
|
|
43
|
+
uint64 timestamp_ns = 4;
|
|
44
|
+
string content_type = 5;
|
|
45
|
+
map<string, string> metadata = 6;
|
|
46
|
+
bytes content = 7;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
message AppendResponse {
|
|
50
|
+
uint32 namespace_id = 1;
|
|
51
|
+
uint64 stream_id = 2;
|
|
52
|
+
uint64 seq = 3;
|
|
53
|
+
uint64 gid = 4; // logdb internal gid (opaque to caller)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
message BatchAppendRequest {
|
|
57
|
+
repeated AppendRequest requests = 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
message AppendBatchResponse {
|
|
61
|
+
repeated AppendResponse records = 1;
|
|
62
|
+
Error error = 2;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
message Error {
|
|
66
|
+
string code = 1;
|
|
67
|
+
string message = 2;
|
|
68
|
+
bool retryable = 3;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Read ────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
message ReadRequest {
|
|
74
|
+
string namespace = 1;
|
|
75
|
+
string stream = 2;
|
|
76
|
+
uint64 seq = 3;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
message ReadResponse {
|
|
80
|
+
Record record = 1;
|
|
81
|
+
bool found = 2;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
message ScanRequest {
|
|
85
|
+
string namespace = 1;
|
|
86
|
+
string stream = 2;
|
|
87
|
+
uint64 from_seq = 3;
|
|
88
|
+
uint64 to_seq = 4; // 0 = durable tail
|
|
89
|
+
uint32 limit = 5;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
message ScanResponse {
|
|
93
|
+
repeated Record records = 1;
|
|
94
|
+
uint64 next_seq = 2;
|
|
95
|
+
bool has_more = 3;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
message TailRequest {
|
|
99
|
+
string namespace = 1;
|
|
100
|
+
string stream = 2;
|
|
101
|
+
uint64 from_seq = 3;
|
|
102
|
+
uint32 batch_size = 4;
|
|
103
|
+
string consumer_group = 5; // consumer group name (for shared offset tracking)
|
|
104
|
+
string consumer_id = 6; // unique consumer ID within the group
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
message TailResponse {
|
|
108
|
+
repeated Record records = 1;
|
|
109
|
+
uint64 durable_seq = 2;
|
|
110
|
+
bool heartbeat = 3;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Watermark ───────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
message GetWatermarkRequest {
|
|
116
|
+
string namespace = 1; // required
|
|
117
|
+
string stream = 2; // optional
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
message Watermark {
|
|
121
|
+
string namespace = 1;
|
|
122
|
+
string stream = 2;
|
|
123
|
+
uint64 oldest_seq = 3;
|
|
124
|
+
uint64 durable_seq = 4;
|
|
125
|
+
uint64 replicated_seq = 5;
|
|
126
|
+
string node_id = 6;
|
|
127
|
+
string role = 7;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Admin ───────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
message CheckpointRequest {
|
|
133
|
+
uint64 sequence = 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
message CheckpointResponse {}
|
|
137
|
+
|
|
138
|
+
message StatusRequest {}
|
|
139
|
+
|
|
140
|
+
message StatusResponse {
|
|
141
|
+
uint64 durable_sequence = 1;
|
|
142
|
+
uint64 checkpoint = 2;
|
|
143
|
+
uint64 wal_bytes_used = 3;
|
|
144
|
+
uint64 wal_bytes_total = 4;
|
|
145
|
+
string node_id = 5;
|
|
146
|
+
string role = 6;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── VerifyChain ─────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
message VerifyChainRequest {
|
|
152
|
+
string namespace = 1;
|
|
153
|
+
string stream = 2;
|
|
154
|
+
uint64 from_seq = 3;
|
|
155
|
+
uint64 to_seq = 4; // 0 = durable tail
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
message VerifyChainResponse {
|
|
159
|
+
bool ok = 1;
|
|
160
|
+
uint64 verified_from = 2;
|
|
161
|
+
uint64 verified_to = 3;
|
|
162
|
+
uint64 error_at_seq = 4;
|
|
163
|
+
string error_message = 5;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Consumer offset tracking ────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
message CommitOffsetRequest {
|
|
169
|
+
string namespace = 1;
|
|
170
|
+
string stream = 2;
|
|
171
|
+
string consumer_group = 3;
|
|
172
|
+
string consumer_id = 4;
|
|
173
|
+
uint64 committed_seq = 5; // last successfully processed seq
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
message CommitOffsetResponse {
|
|
177
|
+
bool ok = 1;
|
|
178
|
+
string message = 2;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Resolve the committed offset for a consumer
|
|
182
|
+
message GetCommittedOffsetRequest {
|
|
183
|
+
string namespace = 1;
|
|
184
|
+
string stream = 2;
|
|
185
|
+
string consumer_group = 3;
|
|
186
|
+
string consumer_id = 4;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
message GetCommittedOffsetResponse {
|
|
190
|
+
uint64 committed_seq = 1; // 0 = no offset committed yet
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Internal replication service (primary → standby).
|
|
194
|
+
// Transfers raw encoded bytes — independent of the public Record schema.
|
|
195
|
+
service ReplicationService {
|
|
196
|
+
rpc Sync(ReplicationRequest) returns (ReplicationResponse);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
message ReplicationRecord {
|
|
200
|
+
uint64 gid = 1; // logdb global id (preserved on standby)
|
|
201
|
+
uint64 timestamp_ns = 2;
|
|
202
|
+
bytes content = 3; // raw encoded logdbd record (header + user_content)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
message ReplicationRequest {
|
|
206
|
+
string cluster_id = 1;
|
|
207
|
+
uint64 epoch = 2;
|
|
208
|
+
repeated ReplicationRecord records = 3;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
message ReplicationResponse {
|
|
212
|
+
uint64 last_gid = 1; // highest gid applied
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─── Snapshot service (primary → standby full sync) ──────────────────────────
|
|
216
|
+
|
|
217
|
+
service SnapshotService {
|
|
218
|
+
// Standby requests a snapshot; primary streams segment files.
|
|
219
|
+
rpc PullSnapshot(SnapshotRequest) returns (stream SnapshotChunk);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
message SnapshotRequest {
|
|
223
|
+
string cluster_id = 1;
|
|
224
|
+
uint64 epoch = 2;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
message SnapshotChunk {
|
|
228
|
+
string file_name = 1; // e.g. "segment-00000001.log"
|
|
229
|
+
uint64 offset = 2; // byte offset within file
|
|
230
|
+
bytes data = 3; // chunk payload
|
|
231
|
+
bool last = 4; // true if this is the last chunk of the snapshot
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ─── Catalog messages ───────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
message ListNamespacesRequest {}
|
|
237
|
+
|
|
238
|
+
message NamespaceInfo {
|
|
239
|
+
string name = 1;
|
|
240
|
+
uint32 id = 2;
|
|
241
|
+
uint64 stream_count = 3;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
message ListNamespacesResponse {
|
|
245
|
+
repeated NamespaceInfo namespaces = 1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
message ListStreamsRequest {
|
|
249
|
+
string namespace = 1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
message StreamInfo {
|
|
253
|
+
string name = 1;
|
|
254
|
+
uint64 id = 2;
|
|
255
|
+
uint64 first_seq = 3;
|
|
256
|
+
uint64 durable_seq = 4;
|
|
257
|
+
uint64 record_count = 5;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
message ListStreamsResponse {
|
|
261
|
+
repeated StreamInfo streams = 1;
|
|
262
|
+
}
|