@happy0/flumeview-level-custom 0.0.0-stage → 2.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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gordon Martin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # flumeview-level-custom
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@happy0/flumeview-level-custom",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2.0.0",
4
+ "description": "",
5
+ "license": "MIT",
6
+ "author": "Gordon Martin",
7
+ "type": "commonjs",
8
+ "main": "src/index.js",
9
+ "scripts": {
10
+ "test": "vitest run .",
11
+ "prettier": "prettier . --write"
12
+ },
13
+ "devDependencies": {
14
+ "flumecodec": "^0.0.1",
15
+ "flumedb": "^2.1.8",
16
+ "flumelog-memory": "^1.0.2",
17
+ "flumelog-offset": "^3.4.4",
18
+ "prettier": "^3.9.10",
19
+ "pull-abortable": "^4.1.1",
20
+ "vitest": "^5.0.5"
21
+ },
22
+ "dependencies": {
23
+ "charwise": "^3.0.1",
24
+ "level": "^10.0.0",
25
+ "level-read-stream": "^2.0.0",
26
+ "ltgt": "^2.2.1",
27
+ "obv": "^0.0.1",
28
+ "pull-level": "^2.0.4",
29
+ "pull-live": "^1.0.1",
30
+ "pull-pushable": "^2.2.0",
31
+ "pull-stream": "^3.7.0",
32
+ "pull-write": "^1.1.4",
33
+ "stream-to-pull-stream": "^1.7.3"
34
+ }
35
+ }
@@ -0,0 +1,104 @@
1
+ const Obv = require("obv");
2
+ const path = require("path");
3
+ const charwise = require("charwise");
4
+ const { Level } = require("level");
5
+ const pull = require("pull-stream");
6
+ const { createPullStream } = require("./stream/read");
7
+ const { METADATA_KEY } = require("./metadata");
8
+
9
+ module.exports = function (version, createWriter, filter) {
10
+ return function (log, name) {
11
+ const dir = path.dirname(log.filename);
12
+
13
+ const since = Obv();
14
+ const db = createLevelDb(dir, log, name);
15
+ let closed = false;
16
+
17
+ startConsumingLog(db, since, version);
18
+
19
+ let writer;
20
+
21
+ function destroy(cb) {
22
+ cb = cb || (() => {});
23
+ since.set(-1);
24
+ db.clear().then(cb);
25
+ }
26
+
27
+ async function startConsumingLog(db, since) {
28
+ const value = await db.get(METADATA_KEY, { keyEncoding: "utf8" });
29
+
30
+ if (!value) {
31
+ // No existing entry, we're consuming the log from the start
32
+ since.set(-1);
33
+ } else if (value.version === version) {
34
+ // The version has not changed, resume consuming the log from where we left off
35
+ // last time
36
+ since.set(value.since);
37
+ } else {
38
+ // version has changed, wipe db and start over.
39
+ destroy(() => {});
40
+ }
41
+ }
42
+
43
+ return {
44
+ // flumeview interface
45
+ methods: { get: "async", read: "source" },
46
+ since: since,
47
+ createSink: function (cb) {
48
+ writer = createWriter(db, since, version, cb);
49
+
50
+ if (!filter) {
51
+ return writer;
52
+ } else {
53
+ return pull(
54
+ pull.filter((item) => filter(item.value)),
55
+ writer,
56
+ );
57
+ }
58
+ },
59
+ destroy: destroy,
60
+ close: function (cb) {
61
+ cb = cb || (() => {});
62
+
63
+ closed = true;
64
+
65
+ if (writer) {
66
+ writer.abort(() => db.close().then(cb));
67
+ } else {
68
+ db.close().then(cb);
69
+ }
70
+ },
71
+ get: function (key, cb) {
72
+ db.get(key).then((result) => {
73
+ if (result === undefined) {
74
+ cb(result);
75
+ } else {
76
+ cb(null, result);
77
+ }
78
+ });
79
+ },
80
+ /**
81
+ *
82
+ * @param {*} opts The level DB stream key range options ( see 'level-read-stream' library ) as well as an optional 'live' parameter to indicate whether the
83
+ * stream should stay live an emit live updates. IF live, a {sync:true} object will be emitted when the stream is about to emit
84
+ * live values and each item will have a 'type' field (put, del) indicating whether the database operation type as well as the key
85
+ * and value
86
+ * @returns a pull-stream source of database entries satisfying the input range
87
+ */
88
+ read: function (opts) {
89
+ return createPullStream(db, opts);
90
+ },
91
+ };
92
+ };
93
+ };
94
+
95
+ function createLevelDb(dir, log, name) {
96
+ if (!log.filename) {
97
+ throw new Error("flumeview-level-custom can only be used with a file log");
98
+ }
99
+
100
+ return new Level(path.join(dir, name), {
101
+ keyEncoding: charwise,
102
+ valueEncoding: "json",
103
+ });
104
+ }
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ const flumeView = require("./flumeView");
2
+ const mapperBuilder = require("./mapperBuilder");
3
+ const metadata = require("./metadata");
4
+
5
+ module.exports.FlumeViewCustom = flumeView;
6
+ module.exports.mapperBuilder = mapperBuilder;
7
+ module.exports.metadata = metadata;
@@ -0,0 +1,37 @@
1
+ const Write = require("pull-write");
2
+ const { createMetadataUpdateItem } = require("./metadata");
3
+
4
+ module.exports.sequentialMapper = function (asyncMapper) {
5
+ const MAX_ITEM_BUFFER_SIZE = 0;
6
+
7
+ return function (db, since, version, streamEndCb) {
8
+ return Write(
9
+ function write(batch, cb) {
10
+ if (batch.length !== 1) {
11
+ cb(
12
+ new Error(
13
+ "Batch should never have more or less than one item for sequential mapper.",
14
+ ),
15
+ );
16
+ } else {
17
+ const item = batch[0];
18
+ const metadata = createMetadataUpdateItem(version, item.seq);
19
+ const update = asyncMapper(db, item.value);
20
+
21
+ Promise.resolve(update)
22
+ .then((items) => {
23
+ return db.batch([metadata, ...items]);
24
+ })
25
+ .then((_) => {
26
+ since.set(item.seq);
27
+ cb();
28
+ })
29
+ .catch((e) => console.error(e));
30
+ }
31
+ },
32
+ null,
33
+ MAX_ITEM_BUFFER_SIZE,
34
+ streamEndCb,
35
+ );
36
+ };
37
+ };
@@ -0,0 +1,16 @@
1
+ const METADATA_KEY = "\x00";
2
+
3
+ function createMetadataUpdateBatchItem(version, seq) {
4
+ return {
5
+ key: METADATA_KEY,
6
+ value: { version: version, since: seq },
7
+ valueEncoding: "json",
8
+ keyEncoding: "utf8",
9
+ type: "put",
10
+ };
11
+ }
12
+
13
+ module.exports = {
14
+ METADATA_KEY,
15
+ createMetadataUpdateItem: createMetadataUpdateBatchItem,
16
+ };
@@ -0,0 +1,36 @@
1
+ var ltgt = require("ltgt");
2
+
3
+ function eq(a, b) {
4
+ if (Buffer.isBuffer(a) && Buffer.isBuffer(b)) {
5
+ return beq(a, b);
6
+ } else return a === b;
7
+ }
8
+
9
+ module.exports = function post(db, opts, each) {
10
+ if (!each) ((each = opts), (opts = {}));
11
+
12
+ var encode =
13
+ (opts && opts.keyEncoding && opts.keyEncoding.encode) ||
14
+ (db.options && db.options.keyEncoding && db.options.keyEncoding.encode) ||
15
+ function (x) {
16
+ return x;
17
+ };
18
+
19
+ var _opts = ltgt.toLtgt(opts, {}, encode);
20
+
21
+ function cmp(key) {
22
+ return ltgt.contains(_opts, encode(key));
23
+ }
24
+
25
+ function onWrite(ary) {
26
+ ary.forEach(function (op) {
27
+ if (cmp(op.key)) each(op);
28
+ });
29
+ }
30
+
31
+ db.on("write", onWrite);
32
+
33
+ return function () {
34
+ db.removeListener("write", onWrite);
35
+ };
36
+ };
@@ -0,0 +1,48 @@
1
+ const { EntryStream, KeyStream, ValueStream } = require("level-read-stream");
2
+ const toPull = require("stream-to-pull-stream");
3
+ const post = require("./post");
4
+ const pushable = require("pull-pushable");
5
+ const Live = require("pull-live");
6
+
7
+ function old(db, opts) {
8
+ const stream = createStream(db, opts);
9
+ return toPull.read1(stream);
10
+ }
11
+
12
+ function live(db, opts) {
13
+ const p = pushable(function (err) {
14
+ if (opts.onAbort) opts.onAbort(err);
15
+ cleanup();
16
+ });
17
+
18
+ const cleanup = post(db, opts, function (ch) {
19
+ if (opts.keys === false) p.push(ch.value);
20
+ else if (opts.values === false) p.push(ch.key);
21
+ else {
22
+ p.push({ type: ch.type, key: ch.key, value: ch.value });
23
+ }
24
+ });
25
+
26
+ return p;
27
+ }
28
+
29
+ function createStream(db, opts) {
30
+ if (opts.keys && !opts.values) {
31
+ return new KeyStream(db, opts);
32
+ } else if (!opts.keys && opts.values) {
33
+ return new ValueStream(db, opts);
34
+ } else {
35
+ return new EntryStream(db, opts);
36
+ }
37
+ }
38
+
39
+ module.exports.createPullStream = function (db, opts) {
40
+ return Live(
41
+ function (opts) {
42
+ return old(db, opts);
43
+ },
44
+ function (opts) {
45
+ return live(db, opts);
46
+ },
47
+ )(opts);
48
+ };
@@ -0,0 +1,236 @@
1
+ const { FlumeViewCustom, mapperBuilder } = require("../src/index");
2
+ const Flume = require("flumedb");
3
+ const util = require("util");
4
+ const pull = require("pull-stream");
5
+ const Log = require("flumelog-offset");
6
+ const codec = require("flumecodec");
7
+ const { rmSync } = require("fs");
8
+
9
+ function pullCollectPromise(source) {
10
+ return new Promise((resolve, reject) => {
11
+ return pull(
12
+ source,
13
+ pull.collect((err, result) => {
14
+ if (result) {
15
+ resolve(result);
16
+ } else {
17
+ reject(err);
18
+ }
19
+ }),
20
+ );
21
+ });
22
+ }
23
+
24
+ function chessView(flumeDb) {
25
+ async function chessMapper(db, item) {
26
+ // Simplified - in a really scuttlebutt environment we'd have to deal with invite acceptances / game ends arriving
27
+ // out of order from different user's feeds
28
+
29
+ if (item.value.content.type === "chess_invite") {
30
+ const gameId = item.key;
31
+
32
+ const entry = {
33
+ id: gameId,
34
+ inviter: item.value.author,
35
+ invitee: item.value.content.inviting,
36
+ white:
37
+ item.value.content.myColor === "white"
38
+ ? item.value.author
39
+ : item.value.content.inviting,
40
+ black:
41
+ item.value.content.myColor === "black"
42
+ ? item.value.author
43
+ : item.value.content.inviting,
44
+ state: "invite",
45
+ };
46
+
47
+ return [
48
+ {
49
+ type: "put",
50
+ key: `/games/${gameId}`,
51
+ value: entry,
52
+ },
53
+ {
54
+ type: "put",
55
+ key: `/player/inviter/${entry.inviter}/id/${gameId}`,
56
+ value: entry,
57
+ },
58
+ {
59
+ type: "put",
60
+ key: `/player/invited/${entry.invitee}/id/${gameId}`,
61
+ value: entry,
62
+ },
63
+ ];
64
+ } else if (item.value.content.type === "chess_invite_accept") {
65
+ const gameId = item.value.content.root;
66
+ const existingEntry = await db.get(`/games/${gameId}`);
67
+
68
+ existingEntry.state = "accepted";
69
+
70
+ const deletions = [
71
+ {
72
+ type: "del",
73
+ key: `/player/inviter/${existingEntry.inviter}/id/${gameId}`,
74
+ },
75
+ {
76
+ type: "del",
77
+ key: `/player/invited/${existingEntry.invitee}/id/${gameId}`,
78
+ },
79
+ ];
80
+
81
+ const insertions = [
82
+ { type: "put", key: `/games/${gameId}`, value: existingEntry },
83
+ {
84
+ type: "put",
85
+ key: `/player/live_game/${existingEntry.inviter}/game/${gameId}`,
86
+ value: existingEntry,
87
+ },
88
+ {
89
+ type: "put",
90
+ key: `/player/live_game/${existingEntry.invitee}/game/${gameId}`,
91
+ value: existingEntry,
92
+ },
93
+ ];
94
+
95
+ return [...deletions, ...insertions];
96
+ }
97
+ }
98
+
99
+ const chessFlumeView = FlumeViewCustom(
100
+ 1,
101
+ mapperBuilder.sequentialMapper(chessMapper),
102
+ );
103
+
104
+ const db = flumeDb.use("flumeviewCustom", chessFlumeView);
105
+
106
+ const chessView = db["flumeviewCustom"];
107
+ const append = util.promisify(db.append);
108
+
109
+ return {
110
+ append: (item) => {
111
+ return append(item);
112
+ },
113
+ getOutboundInvites: function (inviter) {
114
+ return chessView.read({
115
+ gt: `/player/inviter/${inviter}`,
116
+ lt: `/player/j`,
117
+ });
118
+ },
119
+ getInboundInvites: function (invitee) {
120
+ return chessView.read({
121
+ gt: `/player/invited/${invitee}/`,
122
+ lt: `/player/invited/${invitee}/\uffff`,
123
+ });
124
+ },
125
+ getGamesInProgress: function (playerId) {
126
+ return chessView.read({
127
+ gt: `/player/live_game/${playerId}/`,
128
+ lt: `/player/live_game/${playerId}/\uffff`,
129
+ });
130
+ },
131
+ };
132
+ }
133
+
134
+ describe("End to end scenario", () => {
135
+ let db;
136
+
137
+ afterEach(async () => {
138
+ if (db) {
139
+ db.close(() => {});
140
+ db = null;
141
+ rmSync("/tmp/flumeview-level-custom", { recursive: true, force: true });
142
+ }
143
+ });
144
+
145
+ test("Chess game index scenario invites (non-live)", async () => {
146
+ db = Flume(
147
+ Log("/tmp/flumeview-level-custom/log2.offset", 1024, codec.json),
148
+ );
149
+
150
+ const chess = chessView(db);
151
+
152
+ const items = [
153
+ {
154
+ key: "%Zf7y1aoZ/51P5IzF6GEvXNRkBqs+X7r+hG5djhQfwpM=.sha256",
155
+ value: {
156
+ previous: "%EA7jGV3XotJ3qQRMwpDbswj2xfHkEFCbZtSiXfr6d6E=.sha256",
157
+ sequence: 19942,
158
+ author: "@RJ09Kfs3neEZPrbpbWVDxkN92x9moe3aPusOMOc4S2I=.ed25519",
159
+ timestamp: 1791495457292,
160
+ hash: "sha256",
161
+ content: {
162
+ type: "chess_invite",
163
+ inviting: "@STgkEO48Kucd57i7x0vBqfkz/euvt2XdglwF6gZLtdk=.ed25519",
164
+ myColor: "black",
165
+ branch: [
166
+ "%AxUibLnrkZrzA6bk15LpGGDepPZOR2nEn24Kql0YYho=.sha256",
167
+ "%t/8R6p+0Hkq7vDacYwiMOoKNzRf9DD2Ukhiyhxmsroc=.sha256",
168
+ "%01ksKwoAAkrY+bjYdP1WCgtZHHGGcZ8bXmEoYnUlyn4=.sha256",
169
+ "%9oK3ltfnvH9/7h5sxD6Cu+IYoSXcOddTMPJ5L6dLsUM=.sha256",
170
+ "%ldUBk14tkOXNoLECOP9BYDyCyBh4p9KvfHAIzpwcK9k=.sha256",
171
+ ],
172
+ },
173
+ signature:
174
+ "LSq6Ul3o71i7QlPANr3xEhuazhHhPyByLoCGtxv+Nhu37VeI35UOIjCXGBfUNl+9c4f54y6XRNZXS4/ctQvhAg==.sig.ed25519",
175
+ },
176
+ },
177
+ {
178
+ key: "%GpeF1QW5NEiyMDSnotDYoU/2YDFAVVVNYJDleKpnhtw=.sha256",
179
+ value: {
180
+ previous: "%sat8SZr1QshItA3IZK4e6221k8i+3NOf0JLH6PlgIzw=.sha256",
181
+ sequence: 5561,
182
+ author: "@STgkEO48Kucd57i7x0vBqfkz/euvt2XdglwF6gZLtdk=.ed25519",
183
+ timestamp: 1793634892038,
184
+ hash: "sha256",
185
+ content: {
186
+ type: "chess_invite_accept",
187
+ root: "%Zf7y1aoZ/51P5IzF6GEvXNRkBqs+X7r+hG5djhQfwpM=.sha256",
188
+ branch: "%Zf7y1aoZ/51P5IzF6GEvXNRkBqs+X7r+hG5djhQfwpM=.sha256",
189
+ },
190
+ signature:
191
+ "S/ZvvE9VNtkcI6vMJmijlwU8kDAjKLlWhAMenA/tZQRs1MOsDnl3AySfNMAkNEIJw4pUP0//EeTUOFPQgECwCA==.sig.ed25519",
192
+ },
193
+ timestamp: 1793639323422,
194
+ rts: 1793634892038,
195
+ },
196
+ ];
197
+
198
+ await chess.append(items[0]);
199
+
200
+ const source = chess.getInboundInvites(
201
+ "@STgkEO48Kucd57i7x0vBqfkz/euvt2XdglwF6gZLtdk=.ed25519",
202
+ );
203
+
204
+ const result = await pullCollectPromise(source);
205
+
206
+ expect(result.length).toStrictEqual(1);
207
+
208
+ const invitedSource = chess.getOutboundInvites(
209
+ "@RJ09Kfs3neEZPrbpbWVDxkN92x9moe3aPusOMOc4S2I=.ed25519",
210
+ );
211
+ const invitedResult = await pullCollectPromise(invitedSource);
212
+ expect(invitedResult.length).toStrictEqual(1);
213
+
214
+ const inProgressSource = chess.getGamesInProgress(
215
+ "@RJ09Kfs3neEZPrbpbWVDxkN92x9moe3aPusOMOc4S2I=.ed25519",
216
+ );
217
+ const inProgressResult = await pullCollectPromise(inProgressSource);
218
+ expect(inProgressResult.length).toStrictEqual(0);
219
+
220
+ // After invite acceptance the invites should disappear and there should be an 'in progress' result
221
+ await chess.append(items[1]);
222
+
223
+ const invitedSource2 = chess.getInboundInvites(
224
+ "@RJ09Kfs3neEZPrbpbWVDxkN92x9moe3aPusOMOc4S2I=.ed25519",
225
+ );
226
+ const invitedResult2 = await pullCollectPromise(invitedSource2);
227
+ expect(invitedResult2.length).toStrictEqual(0);
228
+
229
+ const inProgressSource2 = chess.getGamesInProgress(
230
+ "@RJ09Kfs3neEZPrbpbWVDxkN92x9moe3aPusOMOc4S2I=.ed25519",
231
+ );
232
+
233
+ const inProgressResult2 = await pullCollectPromise(inProgressSource2);
234
+ expect(inProgressResult2.length).toStrictEqual(1);
235
+ });
236
+ });
@@ -0,0 +1,352 @@
1
+ const Log = require("flumelog-offset");
2
+ const { mapperBuilder, FlumeViewCustom } = require("../src");
3
+ const Flume = require("flumedb");
4
+ const codec = require("flumecodec");
5
+ const util = require("util");
6
+ const { rmSync } = require("fs");
7
+ const pull = require("pull-stream");
8
+ const Aborter = require("pull-abortable");
9
+
10
+ describe("flumeview-level-custom flumeview builder tests", () => {
11
+ function defaultWriter() {
12
+ return mapperBuilder.sequentialMapper(defaultMapper);
13
+ }
14
+
15
+ function createWriter(mapper) {
16
+ return mapperBuilder.sequentialMapper(mapper);
17
+ }
18
+
19
+ function defaultMapper(db, item) {
20
+ return Promise.resolve([{ type: "put", key: item.id, value: item.value }]);
21
+ }
22
+
23
+ let db = null;
24
+
25
+ afterEach(async () => {
26
+ if (db) {
27
+ db.close(() => {});
28
+ db = null;
29
+ rmSync("/tmp/flumeview-level-custom", { recursive: true, force: true });
30
+ }
31
+ });
32
+
33
+ describe("get", () => {
34
+ test("Test without optional filter function parameter set", async () => {
35
+ db = Flume(
36
+ Log("/tmp/flumeview-level-custom/log2.offset", 1024, codec.json),
37
+ ).use("flumeviewCustom", FlumeViewCustom(1, defaultWriter()));
38
+
39
+ const get = util.promisify(db["flumeviewCustom"].get);
40
+ const append = util.promisify(db.append);
41
+
42
+ await append({ id: "testaroonie", value: "testarino" });
43
+
44
+ const result = await get("testaroonie");
45
+
46
+ expect(result).toStrictEqual("testarino");
47
+ });
48
+
49
+ test("Test with optional filter function parameter set", async () => {
50
+ const filter = (item) => item.type !== "post";
51
+
52
+ db = Flume(
53
+ Log("/tmp/flumeview-level-custom/log.offset", 1024, codec.json),
54
+ ).use("flumeviewCustom", FlumeViewCustom(1, defaultWriter(), filter));
55
+
56
+ const append = util.promisify(db.append);
57
+ const get = util.promisify(db["flumeviewCustom"].get);
58
+
59
+ await append({ id: "testaroonie", value: "value" });
60
+ await append({ id: "testaroonie2", value: "value2", type: "post" });
61
+ await append({ id: "testaroonie3", value: "value3" });
62
+
63
+ const result1 = await get("testaroonie");
64
+ const result2 = await get("testaroonie2");
65
+ const result3 = await get("testaroonie3");
66
+
67
+ expect(result1).toStrictEqual("value");
68
+ expect(result2).toStrictEqual(undefined);
69
+ expect(result3).toStrictEqual("value3");
70
+ });
71
+ });
72
+
73
+ describe("read", () => {
74
+ test("non-live read", async () => {
75
+ db = Flume(
76
+ Log("/tmp/flumeview-level-custom/log.offset", 1024, codec.json),
77
+ ).use("flumeviewCustom", FlumeViewCustom(1, defaultWriter()));
78
+
79
+ const append = util.promisify(db.append);
80
+ const read = db["flumeviewCustom"].read;
81
+
82
+ await append({ id: "testaroonie", value: "value" });
83
+ await append({ id: "testaroonie2", value: "value2", type: "post" });
84
+ await append({ id: "testaroonie3", value: "value3" });
85
+
86
+ const source = read({
87
+ live: false,
88
+ gte: "testaroonie",
89
+ lte: "testaroonie2",
90
+ });
91
+
92
+ const result = await new Promise((resolve, reject) => {
93
+ const result = [];
94
+ pull(
95
+ source,
96
+ pull.drain(
97
+ (item) => result.push(item),
98
+ (err) => {
99
+ err ? reject(err) : resolve(result);
100
+ },
101
+ ),
102
+ );
103
+ });
104
+
105
+ expect(result.length).toStrictEqual(2);
106
+
107
+ expect(result[0]).toStrictEqual({ key: "testaroonie", value: "value" });
108
+ expect(result[1]).toStrictEqual({ key: "testaroonie2", value: "value2" });
109
+ });
110
+
111
+ test("Read live stream", async () => {
112
+ db = Flume(
113
+ Log("/tmp/flumeview-level-custom/log.offset", 1024, codec.json),
114
+ ).use("flumeviewCustom", FlumeViewCustom(1, defaultWriter()));
115
+
116
+ const append = util.promisify(db.append);
117
+ const read = db["flumeviewCustom"].read;
118
+
119
+ await append({ id: "testaroonie", value: "value" });
120
+ await append({ id: "testaroonie2", value: "value2", type: "post" });
121
+
122
+ const source = read({
123
+ live: true,
124
+ gte: "testaroonie",
125
+ });
126
+
127
+ const aborter = Aborter();
128
+
129
+ const resultPromise = new Promise((resolve, reject) => {
130
+ const result = [];
131
+ pull(
132
+ source,
133
+ aborter,
134
+ pull.drain(
135
+ (item) => {
136
+ if (item.value === "value") {
137
+ // Wait until the stream gets going so that we get this as a 'live' stream event
138
+ append({ id: "testaroonie3", value: "value3" });
139
+ }
140
+
141
+ if (item.value === "value3") {
142
+ aborter.abort();
143
+ }
144
+
145
+ result.push(item);
146
+ },
147
+ (err) => {
148
+ err ? reject(err) : resolve(result);
149
+ },
150
+ ),
151
+ );
152
+ });
153
+
154
+ const result = await resultPromise;
155
+
156
+ // One item is a 'sync' message indicating the stream is now live
157
+ expect(result.length).toStrictEqual(4);
158
+
159
+ expect(result[0]).toStrictEqual({ key: "testaroonie", value: "value" });
160
+ expect(result[1]).toStrictEqual({ key: "testaroonie2", value: "value2" });
161
+ expect(result[3]).toStrictEqual({
162
+ key: "testaroonie3",
163
+ value: "value3",
164
+ type: "put",
165
+ });
166
+ });
167
+
168
+ test("Read live stream (deletion)", async () => {
169
+ function mapper(db, item) {
170
+ const type = item.type || "put";
171
+ return [{ type: type, key: item.id, value: item.value, type: type }];
172
+ }
173
+
174
+ db = Flume(
175
+ Log("/tmp/flumeview-level-custom/log.offset", 1024, codec.json),
176
+ ).use("flumeviewCustom", FlumeViewCustom(1, createWriter(mapper)));
177
+
178
+ const append = util.promisify(db.append);
179
+ const read = db["flumeviewCustom"].read;
180
+
181
+ await append({ id: "testaroonie", value: "value" });
182
+ await append({ id: "testaroonie2", value: { test: "value2" } });
183
+
184
+ const source = read({
185
+ live: true,
186
+ gte: "testaroonie",
187
+ });
188
+
189
+ const aborter = Aborter();
190
+
191
+ const resultPromise = new Promise((resolve, reject) => {
192
+ const result = [];
193
+ pull(
194
+ source,
195
+ aborter,
196
+ pull.drain(
197
+ (item) => {
198
+ if (item.key === "testaroonie") {
199
+ // Append once we're reading from the stream so that it appears once we go live
200
+ append({ id: "testaroonie", type: "del" });
201
+ }
202
+
203
+ if (item.type === "del") {
204
+ aborter.abort();
205
+ }
206
+
207
+ result.push(item);
208
+ },
209
+ (err) => {
210
+ err ? reject(err) : resolve(result);
211
+ },
212
+ ),
213
+ );
214
+ });
215
+
216
+ const result = await resultPromise;
217
+
218
+ expect(result.length).toStrictEqual(4);
219
+
220
+ expect(result[0]).toStrictEqual({ key: "testaroonie", value: "value" });
221
+ expect(result[1]).toStrictEqual({
222
+ key: "testaroonie2",
223
+ value: { test: "value2" },
224
+ });
225
+ // Sync message expected inbetween
226
+ expect(result[3]).toStrictEqual({
227
+ key: "testaroonie",
228
+ type: "del",
229
+ value: undefined,
230
+ });
231
+ });
232
+
233
+ test("Read live stream (update key)", () => {});
234
+
235
+ test("Read stream (keys only)", () => {});
236
+
237
+ test("Read stream (values only)", () => {});
238
+
239
+ test("Read stream (live, keys only)", () => {});
240
+
241
+ test("Read stream (live, values only)", () => {});
242
+
243
+ test("Read stream live, custom key encoding", () => {});
244
+ });
245
+
246
+ describe("Offset tracking", () => {
247
+ test("Starts from last offset reached on startup", async () => {
248
+ db = Flume(
249
+ Log("/tmp/flumeview-level-custom/log2.offset", 1024, codec.json),
250
+ ).use("flumeviewCustom", FlumeViewCustom(1, defaultWriter()));
251
+
252
+ const get = util.promisify(db["flumeviewCustom"].get);
253
+ const append = util.promisify(db.append);
254
+
255
+ await append({ id: "testaroonie", value: "testarino" });
256
+
257
+ const result = await get("testaroonie");
258
+
259
+ expect(result).toStrictEqual("testarino");
260
+
261
+ // Append a couple of new items before mounting the view
262
+
263
+ db.close(() => {});
264
+
265
+ db = Flume(
266
+ Log("/tmp/flumeview-level-custom/log2.offset", 1024, codec.json),
267
+ );
268
+
269
+ const append2 = util.promisify(db.append);
270
+ await append2({ id: "testaroonie2", value: "testarino2" });
271
+ await append2({ id: "testaroonie3", value: "testarino3" });
272
+
273
+ db.close(() => {});
274
+
275
+ db = Flume(
276
+ Log("/tmp/flumeview-level-custom/log2.offset", 1024, codec.json),
277
+ ).use("flumeviewCustom", FlumeViewCustom(1, defaultWriter()));
278
+
279
+ const read = db["flumeviewCustom"].read;
280
+
281
+ const source = read({
282
+ live: false,
283
+ gte: "testaroonie",
284
+ values: true,
285
+ keys: false,
286
+ });
287
+
288
+ // The stream will pause until the indexing has caught up with our new items (since has reached the log seq),
289
+ // then emit all the new items
290
+ const result2 = await new Promise((resolve, reject) => {
291
+ const result = [];
292
+ pull(
293
+ source,
294
+ pull.drain(
295
+ (item) => {
296
+ result.push(item);
297
+ },
298
+ (err) => {
299
+ err ? reject(err) : resolve(result);
300
+ },
301
+ ),
302
+ );
303
+ });
304
+
305
+ expect(result2.length).toStrictEqual(3);
306
+
307
+ expect(result2[0]).toStrictEqual("testarino");
308
+ expect(result2[1]).toStrictEqual("testarino2");
309
+ expect(result2[2]).toStrictEqual("testarino3");
310
+ });
311
+
312
+ test("Rebuilds the index if the new version is greater than the current index", async () => {
313
+ function newSchemaMapper(db, item) {
314
+ return [
315
+ {
316
+ type: "put",
317
+ key: item.id,
318
+ value: { item: item.value, version: 2 },
319
+ },
320
+ ];
321
+ }
322
+
323
+ db = Flume(
324
+ Log("/tmp/flumeview-level-custom/log2.offset", 1024, codec.json),
325
+ ).use("flumeviewCustom", FlumeViewCustom(1, defaultWriter()));
326
+
327
+ const get = util.promisify(db["flumeviewCustom"].get);
328
+ const append = util.promisify(db.append);
329
+
330
+ await append({ id: "testaroonie", value: "testarino" });
331
+
332
+ const result = await get("testaroonie");
333
+
334
+ expect(result).toStrictEqual("testarino");
335
+
336
+ db.close(() => {});
337
+
338
+ db = Flume(
339
+ Log("/tmp/flumeview-level-custom/log2.offset", 1024, codec.json),
340
+ ).use(
341
+ "flumeviewCustom",
342
+ FlumeViewCustom(2, createWriter(newSchemaMapper)),
343
+ );
344
+
345
+ const get2 = util.promisify(db["flumeviewCustom"].get);
346
+
347
+ const updatedResult = await get2("testaroonie");
348
+
349
+ expect(updatedResult).toStrictEqual({ item: "testarino", version: 2 });
350
+ });
351
+ });
352
+ });
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ maxConcurrency: 1,
7
+ },
8
+ });