@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 +21 -0
- package/README.md +1 -3
- package/package.json +33 -4
- package/src/flumeView.js +104 -0
- package/src/index.js +7 -0
- package/src/mapperBuilder.js +37 -0
- package/src/metadata.js +16 -0
- package/src/stream/post.js +36 -0
- package/src/stream/read.js +48 -0
- package/test/e2e.test.js +236 -0
- package/test/index.test.js +352 -0
- package/vitest.config.ts +8 -0
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
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happy0/flumeview-level-custom",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|
package/src/flumeView.js
ADDED
|
@@ -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
|
+
};
|
package/src/metadata.js
ADDED
|
@@ -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
|
+
};
|
package/test/e2e.test.js
ADDED
|
@@ -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
|
+
});
|