@le-space/orbitdb-storage-bridge 0.14.1 → 0.16.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 +77 -316
- package/lib/backends/aleph-pin.js +147 -6
- package/lib/courier-sync.js +299 -13
- package/lib/extract-blocks.js +73 -0
- package/lib/restore-cid.js +147 -55
- package/package.json +3 -1
package/lib/courier-sync.js
CHANGED
|
@@ -17,15 +17,84 @@
|
|
|
17
17
|
* all three. Returns an unsubscribe function.
|
|
18
18
|
*
|
|
19
19
|
* Wire messages (dag-cbor encoded, one per courier payload):
|
|
20
|
-
* { v, tag, p, t: "announce", heads: [hash] }
|
|
20
|
+
* { v, tag, p, t: "announce", heads: [hash], r? } r: please reconcile
|
|
21
21
|
* { v, tag, p, t: "want", cids: [hash], have: [hash] }
|
|
22
22
|
* { v, tag, p, t: "blocks", heads: [hash], blocks: [{ hash, bytes }] }
|
|
23
|
+
* { v, tag, p, t: "op", id: bytes8, o: { op, key, value } } one change
|
|
23
24
|
* { v, tag, p, t: "hello" } is anybody keeping this database out there?
|
|
24
25
|
* { v, tag, p, t: "here" } the answer
|
|
25
26
|
* `tag` is a short hash of the database address, so couriers can be shared
|
|
26
27
|
* between databases without cross-talk while the address itself stays off
|
|
27
28
|
* the air (the mesh reads everything).
|
|
28
29
|
*
|
|
30
|
+
* Two ways to carry a change, and the cheap one is not replication.
|
|
31
|
+
*
|
|
32
|
+
* `announce`/`want`/`blocks` move the log itself: signed entries, their
|
|
33
|
+
* ancestry, the identity that vouches for them. That is real OrbitDB
|
|
34
|
+
* replication — every peer ends up with the same log and the same hashes — and
|
|
35
|
+
* it is what a cold join needs, because a peer that has never seen the database
|
|
36
|
+
* has to be given the manifest and the whole history.
|
|
37
|
+
*
|
|
38
|
+
* It is also expensive out of all proportion to a todo. The operation inside
|
|
39
|
+
* that entry — `{op: "PUT", key, value}`, which OrbitDB hands to every
|
|
40
|
+
* `update` listener already — is the whole of what anyone meant to send.
|
|
41
|
+
*
|
|
42
|
+
* Measured over funkpost's Meshtastic courier, framing, ARQ and duty-cycle
|
|
43
|
+
* pacing included, for one further change to a list both sides already hold:
|
|
44
|
+
*
|
|
45
|
+
* | plane | bytes on the air |
|
|
46
|
+
* | ------------ | ---------------- |
|
|
47
|
+
* | `delta` | 1774 B |
|
|
48
|
+
* | `operations` | 86 B |
|
|
49
|
+
*
|
|
50
|
+
* Twenty times. On a carrier moving half a kilobyte a minute that is three and
|
|
51
|
+
* a half minutes against ten seconds, for one ticked box.
|
|
52
|
+
*
|
|
53
|
+
* So `op` ships the operation and lets the far side perform it locally, as its
|
|
54
|
+
* own write, with its own identity. What that buys and what it costs:
|
|
55
|
+
*
|
|
56
|
+
* - The two logs **diverge**: each device holds its own entry for the same
|
|
57
|
+
* change, with a different hash. For a keyvalue database the materialised
|
|
58
|
+
* view still agrees, and once an IP path returns OrbitDB's own sync merges
|
|
59
|
+
* both logs and the divergence becomes history rather than disagreement.
|
|
60
|
+
* - Two devices editing the **same key** while both are offline can show
|
|
61
|
+
* different values until that merge, because each applies the other's
|
|
62
|
+
* operation after its own. A CRDT would not have this; an oplog with
|
|
63
|
+
* last-write-wins does. For a shared todo list it is an acceptable trade
|
|
64
|
+
* and it must be a deliberate one.
|
|
65
|
+
* - **The receiver must be allowed to write.** This is the requirement that
|
|
66
|
+
* surprises: on the delta plane the far side only *stores* an entry someone
|
|
67
|
+
* else signed, so a database whose access controller names one writer
|
|
68
|
+
* replicates fine. Here the far side performs the change as its own write,
|
|
69
|
+
* so an access controller that excludes it refuses the operation outright —
|
|
70
|
+
* *"Key … is not allowed to write to the log"*. `write: ["*"]` makes it
|
|
71
|
+
* work, which is what funkpost's mesh-todo already does.
|
|
72
|
+
*
|
|
73
|
+
* That is also where a pairing between two devices belongs, rather than in
|
|
74
|
+
* a new protocol of its own: two devices that have met put each other's
|
|
75
|
+
* OrbitDB identity in the write set, and "anyone holding the channel key"
|
|
76
|
+
* becomes "these two". The access controller is the authorisation
|
|
77
|
+
* mechanism this plane needs, and it already exists.
|
|
78
|
+
* - Nothing is signed end to end. The receiver vouches for the change with
|
|
79
|
+
* its own identity, so trust rests entirely on the carrier — which on a
|
|
80
|
+
* Meshtastic channel means everyone holding the channel key. The sender id
|
|
81
|
+
* in `p` is not authentication and must never be read as such.
|
|
82
|
+
* - Reconciliation is asked for, not inferred: `announce()` sets `r` on the
|
|
83
|
+
* wire and the far side answers with a delta. An announce without it is
|
|
84
|
+
* noted and left alone, because divergent logs are this plane's ordinary
|
|
85
|
+
* state rather than a gap.
|
|
86
|
+
* - A lost `op` is simply lost; there is no ancestry to notice the hole.
|
|
87
|
+
* Note that `send`'s own comment — that a dropped message is re-derivable
|
|
88
|
+
* because a peer still wanting it asks again — holds for `announce`, `want`
|
|
89
|
+
* and `blocks` and **not** for `op`: an operation the outbox sheds under
|
|
90
|
+
* `maxOutbox` takes its change with it.
|
|
91
|
+
* `announce()` is the repair: it runs the delta plane, which is complete by
|
|
92
|
+
* construction. Cheap path for the ordinary change, expensive path when
|
|
93
|
+
* something has to be made right.
|
|
94
|
+
*
|
|
95
|
+
* `liveUpdates: "operations"` opts in. The default stays `"delta"`, so nothing
|
|
96
|
+
* changes for a consumer that does not ask.
|
|
97
|
+
*
|
|
29
98
|
* `p` is a four-byte sender id, and it is what makes *presence* possible: a
|
|
30
99
|
* carrier can tell you a radio is in range, which is not the question. The
|
|
31
100
|
* question is whether another program is keeping the same database, and only
|
|
@@ -52,6 +121,20 @@ export const COURIER_SYNC_VERSION = 1;
|
|
|
52
121
|
|
|
53
122
|
const TAG_LENGTH = 8;
|
|
54
123
|
|
|
124
|
+
// Eight bytes naming a change, derived from the entry hash so that every
|
|
125
|
+
// retransmission of the same change carries the same id and applies once.
|
|
126
|
+
const CHANGE_ID_LENGTH = 8;
|
|
127
|
+
// How many change ids to remember. A duplicate arriving after this many others
|
|
128
|
+
// is applied twice: one extra entry in the log, not a wrong value.
|
|
129
|
+
const SEEN_OPS = 512;
|
|
130
|
+
// How many operations may wait for a deliberate send before the batch goes as
|
|
131
|
+
// a delta instead. Not a memory limit: the outbox holds `maxOutbox` (32) and
|
|
132
|
+
// sheds the oldest when it overflows, and an operation is the one message here
|
|
133
|
+
// that is *not* re-derivable — so a long batch of them would lose writes
|
|
134
|
+
// quietly. A delta is one message and complete by construction, which is the
|
|
135
|
+
// right shape once a batch is large enough to threaten that.
|
|
136
|
+
const MAX_PENDING_OPS = 16;
|
|
137
|
+
|
|
55
138
|
// Four bytes of sender id: enough that two peers in one conversation collide
|
|
56
139
|
// with probability ~1 in 4 billion, small enough to ride on every message.
|
|
57
140
|
const PEER_ID_LENGTH = 4;
|
|
@@ -185,6 +268,19 @@ function isOplogEntry(value) {
|
|
|
185
268
|
* Reading the whole ancestry locally to avoid transmitting it is a good trade
|
|
186
269
|
* on any carrier: the reads are a blockstore away, the bytes are airtime.
|
|
187
270
|
*/
|
|
271
|
+
/**
|
|
272
|
+
* Eight bytes naming one change, the same on every retransmission of it.
|
|
273
|
+
*
|
|
274
|
+
* Derived from the entry hash rather than from the operation, so that two
|
|
275
|
+
* genuinely separate writes of the same value — ticking a box off and on and
|
|
276
|
+
* off again — stay separate changes, while a duplicate delivery of one write
|
|
277
|
+
* is recognised and applied once.
|
|
278
|
+
*/
|
|
279
|
+
async function changeId(entryHash) {
|
|
280
|
+
const digest = await sha256.digest(new TextEncoder().encode(entryHash));
|
|
281
|
+
return digest.digest.slice(0, CHANGE_ID_LENGTH);
|
|
282
|
+
}
|
|
283
|
+
|
|
188
284
|
async function reachableFrom(db, roots) {
|
|
189
285
|
const held = new Set();
|
|
190
286
|
const queue = [...roots];
|
|
@@ -478,6 +574,11 @@ async function applyDeltaToStores({ blockstore, log, events, delta }) {
|
|
|
478
574
|
* @param {string} [params.address] Database address, required when `db` is not given
|
|
479
575
|
* @param {Object} params.courier The byte courier (see module docs)
|
|
480
576
|
* @param {Object} [params.dbOptions] Extra options for the lazy `orbitdb.open`
|
|
577
|
+
* @param {"delta"|"operations"} [params.liveUpdates="delta"] How a local write
|
|
578
|
+
* travels. `"delta"` announces and lets the peer ask, which moves signed
|
|
579
|
+
* entries and keeps both logs identical. `"operations"` ships the operation
|
|
580
|
+
* itself — thirteen times smaller, at the cost of divergent logs and no
|
|
581
|
+
* end-to-end signature. See the module docs before choosing it.
|
|
481
582
|
* @param {boolean} [params.announceOnLocalUpdate=true] Announce as soon as a
|
|
482
583
|
* local write lands. Default keeps the eager behaviour. Set false where the
|
|
483
584
|
* courier is expensive — a duty-cycled radio, say — and the application would
|
|
@@ -512,6 +613,7 @@ export async function createCourierSync({
|
|
|
512
613
|
dbOptions = {},
|
|
513
614
|
rejoinIntervalMs = 15000,
|
|
514
615
|
announceOnLocalUpdate = true,
|
|
616
|
+
liveUpdates = "delta",
|
|
515
617
|
peerId = randomPeerId(),
|
|
516
618
|
peerTimeoutMs = PEER_TIMEOUT_MS,
|
|
517
619
|
sendTimeoutMs = SEND_TIMEOUT_MS,
|
|
@@ -527,6 +629,9 @@ export async function createCourierSync({
|
|
|
527
629
|
if (!(peerId instanceof Uint8Array) || peerId.length !== PEER_ID_LENGTH) {
|
|
528
630
|
throw new Error(`peerId must be ${PEER_ID_LENGTH} bytes`);
|
|
529
631
|
}
|
|
632
|
+
if (liveUpdates !== "delta" && liveUpdates !== "operations") {
|
|
633
|
+
throw new Error('liveUpdates must be "delta" or "operations"');
|
|
634
|
+
}
|
|
530
635
|
const databaseAddress = address || (db && db.address);
|
|
531
636
|
if (!databaseAddress) {
|
|
532
637
|
throw new Error("Either an open db or a database address is required");
|
|
@@ -542,6 +647,13 @@ export async function createCourierSync({
|
|
|
542
647
|
const peers = new Map(); // sender id (hex) -> when we last heard it
|
|
543
648
|
let lastHeardAt = null; // any traffic for this database, identified or not
|
|
544
649
|
const listeners = { synced: [], applied: [], message: [], error: [] };
|
|
650
|
+
// Written locally and not yet sent, oldest first. Only fills when the
|
|
651
|
+
// operation plane is on and the application has opted out of sending on
|
|
652
|
+
// every write — the batching case, where a button decides.
|
|
653
|
+
const pendingOps = [];
|
|
654
|
+
// Change ids already applied, oldest first. Insertion-ordered, so the oldest
|
|
655
|
+
// key is the first one Map iteration yields.
|
|
656
|
+
const seenOps = new Map();
|
|
545
657
|
let database = db;
|
|
546
658
|
// Opened on first contact but not handed out: the bootstrap is not in it yet.
|
|
547
659
|
// The protocol works on it all the same, so repair stays incremental.
|
|
@@ -726,7 +838,13 @@ export async function createCourierSync({
|
|
|
726
838
|
const ourHeadHashes = async (target = local()) =>
|
|
727
839
|
target ? (await target.log.heads()).map((entry) => entry.hash) : [];
|
|
728
840
|
|
|
729
|
-
|
|
841
|
+
/**
|
|
842
|
+
* @param {boolean} [reconcile] Ask the peer to close the gap, not merely to
|
|
843
|
+
* note where we stand. Only the operation plane distinguishes the two:
|
|
844
|
+
* there, divergent logs are the ordinary state rather than a gap, so a
|
|
845
|
+
* reconciliation has to be asked for or it would run after every change.
|
|
846
|
+
*/
|
|
847
|
+
const announce = async (reconcile = false) => {
|
|
730
848
|
if (!local()) {
|
|
731
849
|
// Nothing local yet — not even the manifest. An announce of empty heads
|
|
732
850
|
// cannot get one from a peer whose log is also empty, so first contact
|
|
@@ -735,7 +853,34 @@ export async function createCourierSync({
|
|
|
735
853
|
await send({ t: "want", cids: [], have: [] });
|
|
736
854
|
return;
|
|
737
855
|
}
|
|
738
|
-
|
|
856
|
+
const message = { t: "announce", heads: await ourHeadHashes() };
|
|
857
|
+
if (reconcile) message.r = true;
|
|
858
|
+
await send(message);
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Everything written since the last deliberate send, as operations.
|
|
863
|
+
*
|
|
864
|
+
* Answers how many went, so the caller can tell "I sent your changes" from
|
|
865
|
+
* "there was nothing to send, so I asked the peer to reconcile" — which is
|
|
866
|
+
* what an announce means when the queue is empty.
|
|
867
|
+
*
|
|
868
|
+
* Past `MAX_PENDING_OPS` the batch goes as a delta instead. Not arbitrary:
|
|
869
|
+
* the outbox holds `maxOutbox` messages and sheds the oldest, and an
|
|
870
|
+
* operation is the one message here that cannot be re-derived, so a long
|
|
871
|
+
* batch of them would lose writes without saying so. One delta is one
|
|
872
|
+
* message and carries all of it.
|
|
873
|
+
*/
|
|
874
|
+
const flushOperations = async () => {
|
|
875
|
+
if (pendingOps.length === 0) return 0;
|
|
876
|
+
const going = pendingOps.splice(0, pendingOps.length);
|
|
877
|
+
if (going.length > MAX_PENDING_OPS) {
|
|
878
|
+
emit("message", { direction: "out", type: "batch-too-long", bytes: 0 });
|
|
879
|
+
await announce(true);
|
|
880
|
+
return going.length;
|
|
881
|
+
}
|
|
882
|
+
for (const entry of going) await shipOperation(entry);
|
|
883
|
+
return going.length;
|
|
739
884
|
};
|
|
740
885
|
|
|
741
886
|
/**
|
|
@@ -769,15 +914,26 @@ export async function createCourierSync({
|
|
|
769
914
|
};
|
|
770
915
|
|
|
771
916
|
const watchLocalUpdates = () => {
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
//
|
|
775
|
-
|
|
917
|
+
// On the delta plane, opting out means not watching at all: the application
|
|
918
|
+
// announces when it decides to. On the operation plane it means something
|
|
919
|
+
// else — the write still has to be *remembered*, or the deliberate send has
|
|
920
|
+
// nothing to send. So the hook goes on either way there, and only what it
|
|
921
|
+
// does with the entry changes.
|
|
922
|
+
//
|
|
923
|
+
// Incoming announces are still answered in both cases: going quiet must not
|
|
924
|
+
// mean going deaf.
|
|
925
|
+
if (!announceOnLocalUpdate && liveUpdates !== "operations") return;
|
|
776
926
|
if (!database || offUpdate) return;
|
|
777
|
-
const onUpdate = () => {
|
|
927
|
+
const onUpdate = (entry) => {
|
|
778
928
|
if (applying) return; // courier-applied entries already end in an announce
|
|
929
|
+
if (liveUpdates === "operations" && !announceOnLocalUpdate) {
|
|
930
|
+
pendingOps.push(entry);
|
|
931
|
+
return; // waits for the application to ask
|
|
932
|
+
}
|
|
779
933
|
queue = queue
|
|
780
|
-
.then(() =>
|
|
934
|
+
.then(() =>
|
|
935
|
+
liveUpdates === "operations" ? shipOperation(entry) : announceSoon(),
|
|
936
|
+
)
|
|
781
937
|
.catch((error) => emit("error", error));
|
|
782
938
|
};
|
|
783
939
|
database.events.on("update", onUpdate);
|
|
@@ -796,6 +952,16 @@ export async function createCourierSync({
|
|
|
796
952
|
);
|
|
797
953
|
return;
|
|
798
954
|
}
|
|
955
|
+
// On the operation plane both sides write their own entry for the same
|
|
956
|
+
// change, so the peer's heads being strangers to us is the ordinary state
|
|
957
|
+
// and not a gap to close. Without this, the acknowledgement that ends a
|
|
958
|
+
// blocks delivery reads as a reconcile request, and on logs that are
|
|
959
|
+
// permanently divergent each answer earns another: measured over the memory
|
|
960
|
+
// courier at nine messages for one reconcile against six with it. Both
|
|
961
|
+
// terminate — this is chatter, not a loop — and it does not change what an
|
|
962
|
+
// operation costs, which is one message either way.
|
|
963
|
+
if (liveUpdates === "operations" && !message.r) return;
|
|
964
|
+
|
|
799
965
|
const ours = await ourHeadHashes();
|
|
800
966
|
const theirSet = new Set(theirHeads);
|
|
801
967
|
const theyLack = ours.filter((hash) => !theirSet.has(hash));
|
|
@@ -887,8 +1053,114 @@ export async function createCourierSync({
|
|
|
887
1053
|
emit("synced", { joined: result.joined, entries: result.entries });
|
|
888
1054
|
}
|
|
889
1055
|
// Tells the peer where we now stand — their diff turns empty and the
|
|
890
|
-
// exchange goes quiet; doubles as an end-to-end acknowledgement.
|
|
891
|
-
|
|
1056
|
+
// exchange goes quiet; doubles as an end-to-end acknowledgement. Not a
|
|
1057
|
+
// reconcile request: two peers answering each other's would never stop.
|
|
1058
|
+
await announce(false);
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Remember a change id. Answers whether it was new.
|
|
1063
|
+
*
|
|
1064
|
+
* Our own changes are remembered as they go out, so a mesh repeating us —
|
|
1065
|
+
* or a peer echoing the operation onward — cannot make us perform our own
|
|
1066
|
+
* write a second time.
|
|
1067
|
+
*/
|
|
1068
|
+
const rememberOp = (id) => {
|
|
1069
|
+
const key = hex(id);
|
|
1070
|
+
if (seenOps.has(key)) return false;
|
|
1071
|
+
seenOps.set(key, Date.now());
|
|
1072
|
+
while (seenOps.size > SEEN_OPS) seenOps.delete(seenOps.keys().next().value);
|
|
1073
|
+
return true;
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Perform someone else's change as our own write.
|
|
1078
|
+
*
|
|
1079
|
+
* Only the operations a keyvalue, documents or events database emits. A type
|
|
1080
|
+
* whose operations are named differently is not carried this way, and saying
|
|
1081
|
+
* so out loud is better than writing something that silently does nothing.
|
|
1082
|
+
*/
|
|
1083
|
+
const performOperation = async (target, op) => {
|
|
1084
|
+
const run = () => {
|
|
1085
|
+
if (op.op === "PUT") return target.put(op.key, op.value);
|
|
1086
|
+
if (op.op === "DEL") return target.del(op.key);
|
|
1087
|
+
if (op.op === "ADD") return target.add(op.value);
|
|
1088
|
+
throw new Error(`cannot perform operation ${op.op} over the courier`);
|
|
1089
|
+
};
|
|
1090
|
+
try {
|
|
1091
|
+
return await run();
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
// The refusal every consumer of this plane meets first, and OrbitDB's own
|
|
1094
|
+
// message does not say why it is happening here rather than on the delta
|
|
1095
|
+
// plane. Name the cause where it is read.
|
|
1096
|
+
if (/not allowed to write/i.test(error?.message || "")) {
|
|
1097
|
+
throw new Error(
|
|
1098
|
+
"the operation plane performs the change as a local write, so this " +
|
|
1099
|
+
"device must be in the database's write set — open it with an " +
|
|
1100
|
+
'access controller that admits it (mesh-todo uses write: ["*"]), ' +
|
|
1101
|
+
'or stay on liveUpdates: "delta"',
|
|
1102
|
+
{ cause: error },
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
throw error;
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
/** One local change, on its way as an operation rather than as a log entry. */
|
|
1110
|
+
const shipOperation = async (entry) => {
|
|
1111
|
+
const op = entry && entry.payload;
|
|
1112
|
+
if (!op || typeof op.op !== "string" || !entry.hash) return;
|
|
1113
|
+
const id = await changeId(entry.hash);
|
|
1114
|
+
rememberOp(id); // ours: never perform it on ourselves
|
|
1115
|
+
post({ t: "op", id, o: op });
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
const handleOperation = async (message) => {
|
|
1119
|
+
const id = message.id;
|
|
1120
|
+
const op = message.o;
|
|
1121
|
+
if (!(id instanceof Uint8Array) || id.length !== CHANGE_ID_LENGTH) return;
|
|
1122
|
+
if (!op || typeof op.op !== "string") return;
|
|
1123
|
+
|
|
1124
|
+
const target = local();
|
|
1125
|
+
// An operation carries no manifest, so it cannot bootstrap a database. Ask
|
|
1126
|
+
// for the whole thing instead and let the delta plane answer.
|
|
1127
|
+
if (!target) {
|
|
1128
|
+
post({ t: "want", cids: [], have: [] }, { to: senderOf(message) });
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const report = (applied) =>
|
|
1133
|
+
emit("applied", {
|
|
1134
|
+
complete: true,
|
|
1135
|
+
heads: 1,
|
|
1136
|
+
missing: 0,
|
|
1137
|
+
joined: applied ? 1 : 0,
|
|
1138
|
+
held: applied ? 0 : 1,
|
|
1139
|
+
absent: 0,
|
|
1140
|
+
malformed: 0,
|
|
1141
|
+
refused: 0,
|
|
1142
|
+
via: "operation",
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
// Duplicates are ordinary on a broadcast carrier with retransmission, and
|
|
1146
|
+
// performing one twice would put a second entry in the log for a change
|
|
1147
|
+
// that already happened.
|
|
1148
|
+
if (!rememberOp(id)) return report(false);
|
|
1149
|
+
|
|
1150
|
+
// The write we are about to make fires the database's own "update", which
|
|
1151
|
+
// is what an application listens to — so the far side's list moves without
|
|
1152
|
+
// anything else being wired. The guard is what stops that same event from
|
|
1153
|
+
// sending the change straight back out.
|
|
1154
|
+
applying = true;
|
|
1155
|
+
try {
|
|
1156
|
+
await performOperation(target, op);
|
|
1157
|
+
} finally {
|
|
1158
|
+
applying = false;
|
|
1159
|
+
}
|
|
1160
|
+
// No "synced": that event carries the entries that were joined, and nothing
|
|
1161
|
+
// was joined here — the database performed a write of its own. "applied"
|
|
1162
|
+
// is where a courier delivery reports what it came to.
|
|
1163
|
+
report(true);
|
|
892
1164
|
};
|
|
893
1165
|
|
|
894
1166
|
const handlePayload = (bytes) => {
|
|
@@ -917,6 +1189,7 @@ export async function createCourierSync({
|
|
|
917
1189
|
if (message.t === "announce") return handleAnnounce(message);
|
|
918
1190
|
if (message.t === "want") return handleWant(message);
|
|
919
1191
|
if (message.t === "blocks") return handleBlocks(message);
|
|
1192
|
+
if (message.t === "op") return handleOperation(message);
|
|
920
1193
|
})
|
|
921
1194
|
.catch((error) => emit("error", error));
|
|
922
1195
|
};
|
|
@@ -935,7 +1208,9 @@ export async function createCourierSync({
|
|
|
935
1208
|
started = true;
|
|
936
1209
|
unsubscribe = courier.onPayload(handlePayload);
|
|
937
1210
|
watchLocalUpdates();
|
|
938
|
-
|
|
1211
|
+
// A start reconciles: whatever happened while this program was not
|
|
1212
|
+
// running is exactly what the delta plane is for. Once per session.
|
|
1213
|
+
await announce(true);
|
|
939
1214
|
// A joiner that has not bootstrapped keeps re-asking on its own until
|
|
940
1215
|
// the database opens — so a bootstrap the lossy channel dropped heals
|
|
941
1216
|
// without the user pressing "join" again. Cleared the moment the
|
|
@@ -953,7 +1228,18 @@ export async function createCourierSync({
|
|
|
953
1228
|
}
|
|
954
1229
|
},
|
|
955
1230
|
/** Re-announce — recovery poke after suspected loss. */
|
|
956
|
-
|
|
1231
|
+
/**
|
|
1232
|
+
* What an application's "send" button means.
|
|
1233
|
+
*
|
|
1234
|
+
* With changes waiting, it sends those — that is what the press was about.
|
|
1235
|
+
* With none, it is a request to reconcile, which is what it always was.
|
|
1236
|
+
* Internal announces never flush: the acknowledgement that ends a blocks
|
|
1237
|
+
* delivery must not spend somebody's queued writes on its own.
|
|
1238
|
+
*/
|
|
1239
|
+
announce: async () => {
|
|
1240
|
+
if ((await flushOperations()) > 0) return;
|
|
1241
|
+
await announce(true);
|
|
1242
|
+
},
|
|
957
1243
|
/** This instance's sender id, as it appears on the wire. */
|
|
958
1244
|
peerId: hex(peerId),
|
|
959
1245
|
/**
|
package/lib/extract-blocks.js
CHANGED
|
@@ -256,3 +256,76 @@ export async function extractDatabaseBlocks(database, options = {}) {
|
|
|
256
256
|
logger.info(` 📊 Extracted ${blocks.size} total blocks`);
|
|
257
257
|
return { blocks, blockSources, manifestCID };
|
|
258
258
|
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Several databases in one backup: every block of each, in one Map, and the
|
|
262
|
+
* metadata that names them all.
|
|
263
|
+
*
|
|
264
|
+
* The metadata is the shape `backupDatabaseCAR` writes and `isValidMetadata`
|
|
265
|
+
* accepts — `databases` was always a list, and here it finally holds more than
|
|
266
|
+
* one. Each entry also names the database's **heads**: `restoreFromBlocks` can
|
|
267
|
+
* find them from the blocks alone, but a stated head says which entries the
|
|
268
|
+
* writer considered current when the backup ran, and costs a few bytes.
|
|
269
|
+
*
|
|
270
|
+
* Blocks two databases share — a writer's identity — are kept once: the Map is
|
|
271
|
+
* keyed by the CID string OrbitDB uses, and the same block has the same name.
|
|
272
|
+
*
|
|
273
|
+
* @param {Object[]|Object<string, Object>} databases opened OrbitDB databases,
|
|
274
|
+
* as a list or by name
|
|
275
|
+
* @param {Object} [options]
|
|
276
|
+
* @param {(progress: { stage: "database", index: number, total: number,
|
|
277
|
+
* name: string, address: string, entries: number, blocks: number }) => void}
|
|
278
|
+
* [options.onProgress] called after each database, with what it added
|
|
279
|
+
* @param {number} [options.timestamp] ms since the epoch; injected for tests
|
|
280
|
+
* @returns {Promise<{ blocks: Map<string, { cid: CID, bytes: Uint8Array }>,
|
|
281
|
+
* metadata: Object }>}
|
|
282
|
+
*/
|
|
283
|
+
export async function bundleDatabases(databases, options = {}) {
|
|
284
|
+
const list = Array.isArray(databases) ? databases : Object.values(databases ?? {});
|
|
285
|
+
if (list.length === 0) throw new Error("bundleDatabases needs at least one database");
|
|
286
|
+
|
|
287
|
+
const blocks = new Map();
|
|
288
|
+
const described = [];
|
|
289
|
+
let totalEntries = 0;
|
|
290
|
+
|
|
291
|
+
for (const [index, database] of list.entries()) {
|
|
292
|
+
const before = blocks.size;
|
|
293
|
+
const { blocks: own, manifestCID } = await extractDatabaseBlocks(database);
|
|
294
|
+
for (const [name, block] of own) if (!blocks.has(name)) blocks.set(name, block);
|
|
295
|
+
|
|
296
|
+
const entries = (await database.log.values()).length;
|
|
297
|
+
const heads = (await database.log.heads()).map((head) => head.hash);
|
|
298
|
+
totalEntries += entries;
|
|
299
|
+
described.push({
|
|
300
|
+
address: database.address,
|
|
301
|
+
name: database.name,
|
|
302
|
+
type: database.type,
|
|
303
|
+
manifestCID,
|
|
304
|
+
entryCount: entries,
|
|
305
|
+
heads,
|
|
306
|
+
});
|
|
307
|
+
options.onProgress?.({
|
|
308
|
+
stage: "database",
|
|
309
|
+
index,
|
|
310
|
+
total: list.length,
|
|
311
|
+
name: database.name,
|
|
312
|
+
address: database.address,
|
|
313
|
+
entries,
|
|
314
|
+
blocks: blocks.size - before,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
blocks,
|
|
320
|
+
metadata: {
|
|
321
|
+
version: "1.0",
|
|
322
|
+
timestamp: options.timestamp ?? Date.now(),
|
|
323
|
+
databaseCount: described.length,
|
|
324
|
+
totalBlocks: blocks.size,
|
|
325
|
+
totalEntries,
|
|
326
|
+
// The first database's, so a reader of the one-database shape still finds one.
|
|
327
|
+
manifestCID: described[0].manifestCID,
|
|
328
|
+
databases: described,
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|