node-event-stream 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 +96 -0
- package/dist/consumer/consumer.js +1 -0
- package/dist/core/broker.js +410 -0
- package/dist/interface/interface.js +2 -0
- package/dist/producer/producer.js +1 -0
- package/dist/retention/retentionEngine.js +78 -0
- package/dist/server.js +2 -0
- package/dist/storage/bufferMemory.js +40 -0
- package/dist/storage/diskStorage.js +227 -0
- package/dist/storage/ringBuffer.js +40 -0
- package/dist/storage/secondaryIndex.js +62 -0
- package/dist/storage/storage.js +1 -0
- package/dist/utils/helper.js +6 -0
- package/package.json +19 -0
- package/src/consumer/consumer.ts +0 -0
- package/src/core/broker.ts +469 -0
- package/src/interface/interface.ts +87 -0
- package/src/producer/producer.ts +0 -0
- package/src/retention/retentionEngine.ts +100 -0
- package/src/server.ts +1 -0
- package/src/storage/bufferMemory.ts +39 -0
- package/src/storage/diskStorage.ts +210 -0
- package/src/storage/secondaryIndex.ts +71 -0
- package/src/utils/helper.ts +1 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
const fs = __importStar(require("fs"));
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const MAX_SEGMENT_BYTES = 256 * 1024 * 1024;
|
|
39
|
+
const FLUSH_INTERVAL_MS = 50;
|
|
40
|
+
const FLUSH_BATCH_SIZE = 500;
|
|
41
|
+
class DiskStore {
|
|
42
|
+
constructor(dir, topic) {
|
|
43
|
+
this.dir = dir;
|
|
44
|
+
this.topic = topic;
|
|
45
|
+
this.writeBuffer = [];
|
|
46
|
+
this.currentFd = -1;
|
|
47
|
+
this.currentSegmentSize = 0;
|
|
48
|
+
this.currentSegmentBase = 0;
|
|
49
|
+
this.flushTimer = null;
|
|
50
|
+
this.isFlushing = false;
|
|
51
|
+
this.totalFlushed = 0;
|
|
52
|
+
fs.mkdirSync(this.segDir, { recursive: true });
|
|
53
|
+
this.openOrCreateActiveSegment();
|
|
54
|
+
this.startFlushLoop();
|
|
55
|
+
}
|
|
56
|
+
get segDir() {
|
|
57
|
+
return path.join(this.dir, this.topic);
|
|
58
|
+
}
|
|
59
|
+
listPaths() {
|
|
60
|
+
return fs
|
|
61
|
+
.readdirSync(this.segDir)
|
|
62
|
+
.filter((f) => f.endsWith('.log'))
|
|
63
|
+
.sort()
|
|
64
|
+
.map((f) => path.join(this.segDir, f));
|
|
65
|
+
}
|
|
66
|
+
openSegment(base) {
|
|
67
|
+
if (this.currentFd !== -1)
|
|
68
|
+
fs.closeSync(this.currentFd);
|
|
69
|
+
const file = path.join(this.segDir, String(base).padStart(10, '0') + '.log');
|
|
70
|
+
this.currentFd = fs.openSync(file, 'a');
|
|
71
|
+
this.currentSegmentBase = base;
|
|
72
|
+
this.currentSegmentSize = 0;
|
|
73
|
+
}
|
|
74
|
+
openOrCreateActiveSegment() {
|
|
75
|
+
const files = this.listPaths();
|
|
76
|
+
if (files.length === 0) {
|
|
77
|
+
this.openSegment(0);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const last = files[files.length - 1];
|
|
81
|
+
const base = parseInt(path.basename(last, '.log'), 10);
|
|
82
|
+
const size = fs.statSync(last).size;
|
|
83
|
+
if (size >= MAX_SEGMENT_BYTES)
|
|
84
|
+
this.openSegment(base + 1);
|
|
85
|
+
else {
|
|
86
|
+
this.currentSegmentBase = base;
|
|
87
|
+
this.currentSegmentSize = size;
|
|
88
|
+
this.currentFd = fs.openSync(last, 'a');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
enqueue(record) {
|
|
92
|
+
this.writeBuffer.push(JSON.stringify(record) + '\n');
|
|
93
|
+
if (this.writeBuffer.length >= FLUSH_BATCH_SIZE)
|
|
94
|
+
this.flush();
|
|
95
|
+
}
|
|
96
|
+
startFlushLoop() {
|
|
97
|
+
this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
|
|
98
|
+
if (this.flushTimer.unref)
|
|
99
|
+
this.flushTimer.unref();
|
|
100
|
+
}
|
|
101
|
+
flush() {
|
|
102
|
+
if (this.isFlushing || this.writeBuffer.length === 0)
|
|
103
|
+
return;
|
|
104
|
+
this.isFlushing = true;
|
|
105
|
+
const batch = this.writeBuffer.splice(0);
|
|
106
|
+
const payload = batch.join('');
|
|
107
|
+
const byteLength = Buffer.byteLength(payload, 'utf-8');
|
|
108
|
+
if (this.currentSegmentSize + byteLength >= MAX_SEGMENT_BYTES)
|
|
109
|
+
this.openSegment(this.currentSegmentBase + 1);
|
|
110
|
+
fs.writeSync(this.currentFd, payload, null, 'utf-8');
|
|
111
|
+
this.currentSegmentSize += byteLength;
|
|
112
|
+
this.totalFlushed += batch.length;
|
|
113
|
+
this.isFlushing = false;
|
|
114
|
+
}
|
|
115
|
+
*readByOffsets(offsets) {
|
|
116
|
+
if (offsets.size === 0)
|
|
117
|
+
return;
|
|
118
|
+
const remaining = new Set(offsets);
|
|
119
|
+
for (const file of this.listPaths()) {
|
|
120
|
+
if (remaining.size === 0)
|
|
121
|
+
break;
|
|
122
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
123
|
+
for (const line of content.split('\n')) {
|
|
124
|
+
if (!line.trim())
|
|
125
|
+
continue;
|
|
126
|
+
try {
|
|
127
|
+
const rec = JSON.parse(line);
|
|
128
|
+
if (remaining.has(rec.offset)) {
|
|
129
|
+
yield rec;
|
|
130
|
+
remaining.delete(rec.offset);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (_a) {
|
|
134
|
+
/* skip */
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
*replayAll() {
|
|
140
|
+
for (const file of this.listPaths()) {
|
|
141
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
142
|
+
for (const line of content.split('\n')) {
|
|
143
|
+
if (!line.trim())
|
|
144
|
+
continue;
|
|
145
|
+
try {
|
|
146
|
+
yield JSON.parse(line);
|
|
147
|
+
}
|
|
148
|
+
catch (_a) {
|
|
149
|
+
/* skip */
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
buildSegmentMetas() {
|
|
155
|
+
const files = this.listPaths();
|
|
156
|
+
return files.map((file) => {
|
|
157
|
+
var _a, _b, _c;
|
|
158
|
+
const base = parseInt(path.basename(file, '.log'), 10);
|
|
159
|
+
const stat = fs.statSync(file);
|
|
160
|
+
let first = null;
|
|
161
|
+
let last = null;
|
|
162
|
+
let count = 0;
|
|
163
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
164
|
+
for (const line of content.split('\n')) {
|
|
165
|
+
if (!line.trim())
|
|
166
|
+
continue;
|
|
167
|
+
try {
|
|
168
|
+
const rec = JSON.parse(line);
|
|
169
|
+
if (!first)
|
|
170
|
+
first = rec;
|
|
171
|
+
last = rec;
|
|
172
|
+
count++;
|
|
173
|
+
}
|
|
174
|
+
catch (_d) {
|
|
175
|
+
/* skip */
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
filePath: file,
|
|
180
|
+
baseOffset: base,
|
|
181
|
+
lastOffset: (_a = last === null || last === void 0 ? void 0 : last.offset) !== null && _a !== void 0 ? _a : -1,
|
|
182
|
+
sizeBytes: stat.size,
|
|
183
|
+
oldestTimestamp: (_b = first === null || first === void 0 ? void 0 : first.timestamp) !== null && _b !== void 0 ? _b : 0,
|
|
184
|
+
newestTimestamp: (_c = last === null || last === void 0 ? void 0 : last.timestamp) !== null && _c !== void 0 ? _c : 0,
|
|
185
|
+
recordCount: count,
|
|
186
|
+
isActive: base === this.currentSegmentBase,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
deleteSegment(baseOffset) {
|
|
191
|
+
const file = path.join(this.segDir, String(baseOffset).padStart(10, '0') + '.log');
|
|
192
|
+
if (baseOffset === this.currentSegmentBase) {
|
|
193
|
+
console.warn(`[bus] skipping active segment ${baseOffset}`);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const size = fs.statSync(file).size;
|
|
198
|
+
fs.unlinkSync(file);
|
|
199
|
+
return size;
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
console.error(`[bus] failed to delete segment ${file}:`, e);
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
get totalDiskBytes() {
|
|
207
|
+
return this.listPaths().reduce((s, f) => {
|
|
208
|
+
try {
|
|
209
|
+
return s + fs.statSync(f).size;
|
|
210
|
+
}
|
|
211
|
+
catch (_a) {
|
|
212
|
+
return s;
|
|
213
|
+
}
|
|
214
|
+
}, 0);
|
|
215
|
+
}
|
|
216
|
+
get segmentCount() {
|
|
217
|
+
return this.listPaths().length;
|
|
218
|
+
}
|
|
219
|
+
close() {
|
|
220
|
+
this.flush();
|
|
221
|
+
if (this.flushTimer)
|
|
222
|
+
clearInterval(this.flushTimer);
|
|
223
|
+
if (this.currentFd !== -1)
|
|
224
|
+
fs.closeSync(this.currentFd);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
exports.default = DiskStore;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
class BufferMemory {
|
|
4
|
+
constructor(capacity) {
|
|
5
|
+
this.capacity = capacity;
|
|
6
|
+
this.head = 0;
|
|
7
|
+
this._size = 0;
|
|
8
|
+
this.buffer = new Array(capacity);
|
|
9
|
+
}
|
|
10
|
+
push(record) {
|
|
11
|
+
this.buffer[this.head] = record;
|
|
12
|
+
this.head = (this.head + 1) % this.capacity;
|
|
13
|
+
if (this._size < this.capacity)
|
|
14
|
+
this._size++;
|
|
15
|
+
}
|
|
16
|
+
*[Symbol.iterator]() {
|
|
17
|
+
if (this._size === 0)
|
|
18
|
+
return;
|
|
19
|
+
const start = this._size < this.capacity ? 0 : this.head;
|
|
20
|
+
for (let i = 0; i < this._size; i++) {
|
|
21
|
+
const rec = this.buffer[(start + i) % this.capacity];
|
|
22
|
+
if (rec !== undefined)
|
|
23
|
+
yield rec;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
*from(fromOffset) {
|
|
27
|
+
for (const rec of this)
|
|
28
|
+
if (rec.offset >= fromOffset)
|
|
29
|
+
yield rec;
|
|
30
|
+
}
|
|
31
|
+
get size() {
|
|
32
|
+
return this._size;
|
|
33
|
+
}
|
|
34
|
+
get minOffset() {
|
|
35
|
+
for (const r of this)
|
|
36
|
+
return r.offset;
|
|
37
|
+
return -1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.default = BufferMemory;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
class SecondaryIndex {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.keyIndex = new Map();
|
|
6
|
+
this.timeIndex = [];
|
|
7
|
+
this.offsetMap = new Map();
|
|
8
|
+
}
|
|
9
|
+
insert(entry) {
|
|
10
|
+
if (!this.keyIndex.has(entry.key))
|
|
11
|
+
this.keyIndex.set(entry.key, []);
|
|
12
|
+
this.keyIndex.get(entry.key).push(entry.offset);
|
|
13
|
+
this.timeIndex.push([entry.timestamp, entry.offset]);
|
|
14
|
+
this.offsetMap.set(entry.offset, entry);
|
|
15
|
+
}
|
|
16
|
+
purgeBelow(minOffset) {
|
|
17
|
+
let dropped = 0;
|
|
18
|
+
for (const [offset] of this.offsetMap) {
|
|
19
|
+
if (offset < minOffset) {
|
|
20
|
+
this.offsetMap.delete(offset);
|
|
21
|
+
dropped++;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (const [key, offsets] of this.keyIndex) {
|
|
25
|
+
const filtered = offsets.filter((o) => o >= minOffset);
|
|
26
|
+
if (filtered.length === 0)
|
|
27
|
+
this.keyIndex.delete(key);
|
|
28
|
+
else
|
|
29
|
+
this.keyIndex.set(key, filtered);
|
|
30
|
+
}
|
|
31
|
+
this.timeIndex = this.timeIndex.filter(([, o]) => o >= minOffset);
|
|
32
|
+
return dropped;
|
|
33
|
+
}
|
|
34
|
+
query(opts) {
|
|
35
|
+
var _a;
|
|
36
|
+
let candidates = opts.key !== undefined
|
|
37
|
+
? ((_a = this.keyIndex.get(opts.key)) !== null && _a !== void 0 ? _a : [])
|
|
38
|
+
: this.timeIndex.map(([, o]) => o);
|
|
39
|
+
if (opts.fromTime !== undefined || opts.toTime !== undefined) {
|
|
40
|
+
candidates = candidates.filter((offset) => {
|
|
41
|
+
const e = this.offsetMap.get(offset);
|
|
42
|
+
if (!e)
|
|
43
|
+
return false;
|
|
44
|
+
if (opts.fromTime !== undefined && e.timestamp < opts.fromTime)
|
|
45
|
+
return false;
|
|
46
|
+
if (opts.toTime !== undefined && e.timestamp > opts.toTime)
|
|
47
|
+
return false;
|
|
48
|
+
return true;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (opts.order === 'desc')
|
|
52
|
+
candidates = [...candidates].reverse();
|
|
53
|
+
return candidates.slice(opts.skip, opts.skip + opts.limit);
|
|
54
|
+
}
|
|
55
|
+
get totalKeys() {
|
|
56
|
+
return this.keyIndex.size;
|
|
57
|
+
}
|
|
58
|
+
get totalEntries() {
|
|
59
|
+
return this.offsetMap.size;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.default = SecondaryIndex;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-event-stream",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lightweight event streaming for Node.js",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node dist/server.js",
|
|
8
|
+
"dev": "nodemon dist/server.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [],
|
|
11
|
+
"author": "",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"nodemon": "^3.1.14"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^25.3.5"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
File without changes
|