@blamejs/blamejs-shop 0.1.34 → 0.1.35
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/CHANGELOG.md +2 -0
- package/README.md +1 -0
- package/lib/asset-manifest.json +7 -3
- package/lib/storefront.js +432 -4
- package/lib/vendor/MANIFEST.json +3 -3
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +89 -2
- package/lib/vendor/blamejs/index.js +3 -0
- package/lib/vendor/blamejs/lib/crdt.js +453 -0
- package/lib/vendor/blamejs/lib/crypto-xwing.js +213 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.3.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.13.4.json +23 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crdt.test.js +158 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-xwing.test.js +120 -0
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"frameworkVersion": "0.13.
|
|
4
|
-
"createdAt": "2026-05-
|
|
3
|
+
"frameworkVersion": "0.13.4",
|
|
4
|
+
"createdAt": "2026-05-27T00:29:21.229Z",
|
|
5
5
|
"exports": {
|
|
6
6
|
"a2a": {
|
|
7
7
|
"type": "object",
|
|
@@ -13699,6 +13699,43 @@
|
|
|
13699
13699
|
}
|
|
13700
13700
|
}
|
|
13701
13701
|
},
|
|
13702
|
+
"crdt": {
|
|
13703
|
+
"type": "object",
|
|
13704
|
+
"members": {
|
|
13705
|
+
"CrdtError": {
|
|
13706
|
+
"type": "function",
|
|
13707
|
+
"arity": 4
|
|
13708
|
+
},
|
|
13709
|
+
"gCounter": {
|
|
13710
|
+
"type": "function",
|
|
13711
|
+
"arity": 1
|
|
13712
|
+
},
|
|
13713
|
+
"gSet": {
|
|
13714
|
+
"type": "function",
|
|
13715
|
+
"arity": 1
|
|
13716
|
+
},
|
|
13717
|
+
"lwwRegister": {
|
|
13718
|
+
"type": "function",
|
|
13719
|
+
"arity": 1
|
|
13720
|
+
},
|
|
13721
|
+
"orMap": {
|
|
13722
|
+
"type": "function",
|
|
13723
|
+
"arity": 1
|
|
13724
|
+
},
|
|
13725
|
+
"orSet": {
|
|
13726
|
+
"type": "function",
|
|
13727
|
+
"arity": 1
|
|
13728
|
+
},
|
|
13729
|
+
"pnCounter": {
|
|
13730
|
+
"type": "function",
|
|
13731
|
+
"arity": 1
|
|
13732
|
+
},
|
|
13733
|
+
"twoPSet": {
|
|
13734
|
+
"type": "function",
|
|
13735
|
+
"arity": 1
|
|
13736
|
+
}
|
|
13737
|
+
}
|
|
13738
|
+
},
|
|
13702
13739
|
"createApp": {
|
|
13703
13740
|
"type": "function",
|
|
13704
13741
|
"arity": 1
|
|
@@ -13999,6 +14036,56 @@
|
|
|
13999
14036
|
"verify": {
|
|
14000
14037
|
"type": "function",
|
|
14001
14038
|
"arity": 3
|
|
14039
|
+
},
|
|
14040
|
+
"xwing": {
|
|
14041
|
+
"type": "object",
|
|
14042
|
+
"members": {
|
|
14043
|
+
"NAME": {
|
|
14044
|
+
"type": "primitive",
|
|
14045
|
+
"valueType": "string"
|
|
14046
|
+
},
|
|
14047
|
+
"SIZES": {
|
|
14048
|
+
"type": "object",
|
|
14049
|
+
"members": {
|
|
14050
|
+
"ciphertext": {
|
|
14051
|
+
"type": "primitive",
|
|
14052
|
+
"valueType": "number"
|
|
14053
|
+
},
|
|
14054
|
+
"publicKey": {
|
|
14055
|
+
"type": "primitive",
|
|
14056
|
+
"valueType": "number"
|
|
14057
|
+
},
|
|
14058
|
+
"secretKey": {
|
|
14059
|
+
"type": "primitive",
|
|
14060
|
+
"valueType": "number"
|
|
14061
|
+
},
|
|
14062
|
+
"sharedSecret": {
|
|
14063
|
+
"type": "primitive",
|
|
14064
|
+
"valueType": "number"
|
|
14065
|
+
}
|
|
14066
|
+
}
|
|
14067
|
+
},
|
|
14068
|
+
"XWingError": {
|
|
14069
|
+
"type": "function",
|
|
14070
|
+
"arity": 4
|
|
14071
|
+
},
|
|
14072
|
+
"combiner": {
|
|
14073
|
+
"type": "function",
|
|
14074
|
+
"arity": 4
|
|
14075
|
+
},
|
|
14076
|
+
"decapsulate": {
|
|
14077
|
+
"type": "function",
|
|
14078
|
+
"arity": 2
|
|
14079
|
+
},
|
|
14080
|
+
"encapsulate": {
|
|
14081
|
+
"type": "function",
|
|
14082
|
+
"arity": 2
|
|
14083
|
+
},
|
|
14084
|
+
"keygen": {
|
|
14085
|
+
"type": "function",
|
|
14086
|
+
"arity": 1
|
|
14087
|
+
}
|
|
14088
|
+
}
|
|
14002
14089
|
}
|
|
14003
14090
|
}
|
|
14004
14091
|
},
|
|
@@ -58,6 +58,7 @@ var crypto = require("./lib/crypto");
|
|
|
58
58
|
// the dedicated lib files; these are thin aliases.
|
|
59
59
|
crypto.hpke = require("./lib/crypto-hpke");
|
|
60
60
|
crypto.oprf = require("./lib/crypto-oprf");
|
|
61
|
+
crypto.xwing = require("./lib/crypto-xwing");
|
|
61
62
|
// Both PQ-HPKE drafts behind one opt-in sub-namespace — see
|
|
62
63
|
// lib/crypto-hpke-pq.js. Operators that need a draft-codepoint
|
|
63
64
|
// shape reach for b.crypto.hpke.pq.connolly / .wg explicitly; the
|
|
@@ -373,6 +374,7 @@ var dualControl = require("./lib/dual-control");
|
|
|
373
374
|
var retention = require("./lib/retention");
|
|
374
375
|
var legalHold = require("./lib/legal-hold");
|
|
375
376
|
var worm = require("./lib/worm");
|
|
377
|
+
var crdt = require("./lib/crdt");
|
|
376
378
|
var network = require("./lib/network");
|
|
377
379
|
var cloudEvents = require("./lib/cloud-events");
|
|
378
380
|
var dsr = require("./lib/dsr");
|
|
@@ -712,6 +714,7 @@ module.exports = {
|
|
|
712
714
|
retention: retention,
|
|
713
715
|
legalHold: legalHold,
|
|
714
716
|
worm: worm,
|
|
717
|
+
crdt: crdt,
|
|
715
718
|
network: network,
|
|
716
719
|
cloudEvents: cloudEvents,
|
|
717
720
|
dsr: dsr,
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.crdt
|
|
4
|
+
* @nav Data
|
|
5
|
+
* @title CRDTs
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Conflict-free Replicated Data Types — data structures that several
|
|
9
|
+
* replicas can update independently, with no coordination, and still
|
|
10
|
+
* converge to the same value once they have all seen each other's state.
|
|
11
|
+
* These are the state-based CvRDTs: each type's <code>merge</code> is a
|
|
12
|
+
* join over a semilattice, so it is commutative, associative, and
|
|
13
|
+
* idempotent — replicas can merge in any order, any number of times, and
|
|
14
|
+
* land on the same result. That makes them the substrate for eventually-
|
|
15
|
+
* consistent state across an active/active cluster, offline-first clients
|
|
16
|
+
* that reconcile on reconnect, or any "last writer need not win, but
|
|
17
|
+
* everyone agrees" counter / set / register / map.
|
|
18
|
+
*
|
|
19
|
+
* Every type exposes the same contract: local mutators (e.g.
|
|
20
|
+
* <code>inc</code>, <code>add</code>, <code>set</code>),
|
|
21
|
+
* <code>merge(other)</code> which returns a new converged instance without
|
|
22
|
+
* mutating either operand, <code>value()</code> for the materialized value,
|
|
23
|
+
* and <code>state()</code> / <code>fromState()</code> for a JSON-
|
|
24
|
+
* serializable form to snapshot (via <code>b.archive</code> /
|
|
25
|
+
* <code>b.backup</code>) or ship to a peer. Each replica carries a
|
|
26
|
+
* <code>replicaId</code> so per-replica contributions stay distinct.
|
|
27
|
+
*
|
|
28
|
+
* This release covers the state-based family — grow-only and PN counters,
|
|
29
|
+
* grow-only / two-phase / observed-remove sets, a last-write-wins register,
|
|
30
|
+
* and an observed-remove map. Operation-based sequence CRDTs (RGA), delta-
|
|
31
|
+
* state mutators, and a live event-bus replicator are not included; the
|
|
32
|
+
* state-based types merge correctly without a causal channel, which is the
|
|
33
|
+
* whole point.
|
|
34
|
+
*
|
|
35
|
+
* @card
|
|
36
|
+
* Conflict-free Replicated Data Types (`b.crdt`) — state-based CvRDT
|
|
37
|
+
* counters, sets, a last-write-wins register, and an observed-remove map
|
|
38
|
+
* whose `merge` is commutative, associative, and idempotent, so replicas
|
|
39
|
+
* converge with no coordination.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
var bCrypto = require("./crypto");
|
|
43
|
+
var safeJson = require("./safe-json");
|
|
44
|
+
var { defineClass } = require("./framework-error");
|
|
45
|
+
|
|
46
|
+
var CrdtError = defineClass("CrdtError", { alwaysPermanent: true });
|
|
47
|
+
|
|
48
|
+
function _replicaId(opts) {
|
|
49
|
+
var id = opts && opts.replicaId;
|
|
50
|
+
if (id == null) return bCrypto.generateToken(8); // allow:raw-byte-literal — random replica-id token length
|
|
51
|
+
if (typeof id !== "string" || id.length === 0) throw new CrdtError("crdt/bad-replica-id", "crdt: replicaId must be a non-empty string");
|
|
52
|
+
return id;
|
|
53
|
+
}
|
|
54
|
+
function _posInt(n, label) {
|
|
55
|
+
if (typeof n !== "number" || !isFinite(n) || n < 0 || Math.floor(n) !== n) throw new CrdtError("crdt/bad-value", "crdt: " + label + " must be a non-negative integer");
|
|
56
|
+
return n;
|
|
57
|
+
}
|
|
58
|
+
function _maxMerge(a, b) {
|
|
59
|
+
var out = {};
|
|
60
|
+
var k;
|
|
61
|
+
for (k in a) if (Object.prototype.hasOwnProperty.call(a, k)) out[k] = a[k];
|
|
62
|
+
for (k in b) if (Object.prototype.hasOwnProperty.call(b, k)) out[k] = (out[k] === undefined || b[k] > out[k]) ? b[k] : out[k];
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @primitive b.crdt.gCounter
|
|
68
|
+
* @signature b.crdt.gCounter(opts?)
|
|
69
|
+
* @since 0.13.4
|
|
70
|
+
* @status stable
|
|
71
|
+
* @compliance soc2
|
|
72
|
+
* @related b.crdt.pnCounter, b.crdt.gSet
|
|
73
|
+
*
|
|
74
|
+
* A grow-only counter: each replica tracks its own increment-only tally, and
|
|
75
|
+
* the value is their sum. <code>merge</code> takes the per-replica maximum, so
|
|
76
|
+
* it converges no matter the order. Increments only — use
|
|
77
|
+
* <code>pnCounter</code> when you also need to decrement.
|
|
78
|
+
*
|
|
79
|
+
* @opts
|
|
80
|
+
* replicaId: string, // this replica's id (default: random)
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* var a = b.crdt.gCounter({ replicaId: "a" }).inc(3);
|
|
84
|
+
* var c = b.crdt.gCounter({ replicaId: "c" }).inc(5);
|
|
85
|
+
* a.merge(c).value(); // → 8
|
|
86
|
+
*/
|
|
87
|
+
function gCounter(opts) {
|
|
88
|
+
opts = opts || {};
|
|
89
|
+
var replicaId = _replicaId(opts);
|
|
90
|
+
var counts = {};
|
|
91
|
+
if (opts._counts) { for (var k in opts._counts) if (Object.prototype.hasOwnProperty.call(opts._counts, k)) counts[k] = opts._counts[k]; }
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
type: "gCounter",
|
|
95
|
+
replicaId: replicaId,
|
|
96
|
+
inc: function (n) { n = n == null ? 1 : _posInt(n, "inc"); counts[replicaId] = (counts[replicaId] || 0) + n; return this; },
|
|
97
|
+
value: function () { var s = 0; for (var k in counts) if (Object.prototype.hasOwnProperty.call(counts, k)) s += counts[k]; return s; },
|
|
98
|
+
state: function () { return { type: "gCounter", counts: Object.assign({}, counts) }; },
|
|
99
|
+
merge: function (other) { return gCounter({ replicaId: replicaId, _counts: _maxMerge(counts, _otherState(other, "gCounter").counts) }); },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
gCounter.fromState = function (s, opts) { _assertState(s, "gCounter"); return gCounter(Object.assign({}, opts, { _counts: s.counts })); };
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @primitive b.crdt.pnCounter
|
|
106
|
+
* @signature b.crdt.pnCounter(opts?)
|
|
107
|
+
* @since 0.13.4
|
|
108
|
+
* @status stable
|
|
109
|
+
* @compliance soc2
|
|
110
|
+
* @related b.crdt.gCounter, b.crdt.lwwRegister
|
|
111
|
+
*
|
|
112
|
+
* A positive-negative counter: two grow-only counters (increments and
|
|
113
|
+
* decrements) whose difference is the value, so it supports both
|
|
114
|
+
* <code>inc</code> and <code>dec</code> and still converges.
|
|
115
|
+
*
|
|
116
|
+
* @opts
|
|
117
|
+
* replicaId: string, // this replica's id (default: random)
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* var a = b.crdt.pnCounter({ replicaId: "a" }).inc(5).dec(2);
|
|
121
|
+
* var c = b.crdt.pnCounter({ replicaId: "c" }).inc(1);
|
|
122
|
+
* a.merge(c).value(); // → 4
|
|
123
|
+
*/
|
|
124
|
+
function pnCounter(opts) {
|
|
125
|
+
opts = opts || {};
|
|
126
|
+
var replicaId = _replicaId(opts);
|
|
127
|
+
var p = gCounter({ replicaId: replicaId, _counts: opts._p });
|
|
128
|
+
var n = gCounter({ replicaId: replicaId, _counts: opts._n });
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
type: "pnCounter",
|
|
132
|
+
replicaId: replicaId,
|
|
133
|
+
inc: function (by) { p.inc(by); return this; },
|
|
134
|
+
dec: function (by) { n.inc(by); return this; },
|
|
135
|
+
value: function () { return p.value() - n.value(); },
|
|
136
|
+
state: function () { return { type: "pnCounter", p: p.state().counts, n: n.state().counts }; },
|
|
137
|
+
merge: function (other) {
|
|
138
|
+
var o = _otherState(other, "pnCounter");
|
|
139
|
+
return pnCounter({ replicaId: replicaId, _p: _maxMerge(p.state().counts, o.p), _n: _maxMerge(n.state().counts, o.n) });
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
pnCounter.fromState = function (s, opts) { _assertState(s, "pnCounter"); return pnCounter(Object.assign({}, opts, { _p: s.p, _n: s.n })); };
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @primitive b.crdt.gSet
|
|
147
|
+
* @signature b.crdt.gSet(opts?)
|
|
148
|
+
* @since 0.13.4
|
|
149
|
+
* @status stable
|
|
150
|
+
* @compliance soc2
|
|
151
|
+
* @related b.crdt.twoPSet, b.crdt.orSet
|
|
152
|
+
*
|
|
153
|
+
* A grow-only set: elements can be added but never removed; <code>merge</code>
|
|
154
|
+
* is set union. The simplest convergent set — reach for <code>orSet</code>
|
|
155
|
+
* when removal is needed. Elements may be strings or JSON-serializable values.
|
|
156
|
+
*
|
|
157
|
+
* @opts
|
|
158
|
+
* replicaId: string, // this replica's id (default: random)
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* var a = b.crdt.gSet().add("x");
|
|
162
|
+
* var c = b.crdt.gSet().add("y");
|
|
163
|
+
* a.merge(c).value(); // → ["x", "y"]
|
|
164
|
+
*/
|
|
165
|
+
function gSet(opts) {
|
|
166
|
+
opts = opts || {};
|
|
167
|
+
var replicaId = _replicaId(opts);
|
|
168
|
+
var els = new Set(opts._els || []);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
type: "gSet",
|
|
172
|
+
replicaId: replicaId,
|
|
173
|
+
add: function (x) { els.add(_key(x)); return this; },
|
|
174
|
+
has: function (x) { return els.has(_key(x)); },
|
|
175
|
+
value: function () { return _decodeKeys(els); },
|
|
176
|
+
state: function () { return { type: "gSet", els: Array.from(els) }; },
|
|
177
|
+
merge: function (other) { var o = _otherState(other, "gSet"); var u = new Set(els); o.els.forEach(function (e) { u.add(e); }); return gSet({ replicaId: replicaId, _els: u }); },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
gSet.fromState = function (s, opts) { _assertState(s, "gSet"); return gSet(Object.assign({}, opts, { _els: s.els })); };
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @primitive b.crdt.twoPSet
|
|
184
|
+
* @signature b.crdt.twoPSet(opts?)
|
|
185
|
+
* @since 0.13.4
|
|
186
|
+
* @status stable
|
|
187
|
+
* @compliance soc2
|
|
188
|
+
* @related b.crdt.gSet, b.crdt.orSet
|
|
189
|
+
*
|
|
190
|
+
* A two-phase set: an add-set and a remove-set (tombstones). An element can be
|
|
191
|
+
* added and removed, but once removed it can never be re-added — remove wins
|
|
192
|
+
* permanently. When re-adding must work, use <code>orSet</code>.
|
|
193
|
+
*
|
|
194
|
+
* @opts
|
|
195
|
+
* replicaId: string, // this replica's id (default: random)
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* var s = b.crdt.twoPSet().add("a").add("b").remove("a");
|
|
199
|
+
* s.value(); // → ["b"]
|
|
200
|
+
*/
|
|
201
|
+
function twoPSet(opts) {
|
|
202
|
+
opts = opts || {};
|
|
203
|
+
var replicaId = _replicaId(opts);
|
|
204
|
+
var adds = new Set(opts._adds || []);
|
|
205
|
+
var removes = new Set(opts._removes || []);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
type: "twoPSet",
|
|
209
|
+
replicaId: replicaId,
|
|
210
|
+
add: function (x) { adds.add(_key(x)); return this; },
|
|
211
|
+
remove: function (x) { var k = _key(x); if (adds.has(k)) removes.add(k); return this; },
|
|
212
|
+
has: function (x) { var k = _key(x); return adds.has(k) && !removes.has(k); },
|
|
213
|
+
value: function () { var live = new Set(); adds.forEach(function (k) { if (!removes.has(k)) live.add(k); }); return _decodeKeys(live); },
|
|
214
|
+
state: function () { return { type: "twoPSet", adds: Array.from(adds), removes: Array.from(removes) }; },
|
|
215
|
+
merge: function (other) {
|
|
216
|
+
var o = _otherState(other, "twoPSet");
|
|
217
|
+
var a = new Set(adds), r = new Set(removes);
|
|
218
|
+
o.adds.forEach(function (e) { a.add(e); });
|
|
219
|
+
o.removes.forEach(function (e) { r.add(e); });
|
|
220
|
+
return twoPSet({ replicaId: replicaId, _adds: a, _removes: r });
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
twoPSet.fromState = function (s, opts) { _assertState(s, "twoPSet"); return twoPSet(Object.assign({}, opts, { _adds: s.adds, _removes: s.removes })); };
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @primitive b.crdt.orSet
|
|
228
|
+
* @signature b.crdt.orSet(opts?)
|
|
229
|
+
* @since 0.13.4
|
|
230
|
+
* @status stable
|
|
231
|
+
* @compliance soc2
|
|
232
|
+
* @related b.crdt.gSet, b.crdt.twoPSet, b.crdt.orMap
|
|
233
|
+
*
|
|
234
|
+
* An observed-remove set: each add stamps a unique tag, and remove tombstones
|
|
235
|
+
* the tags it has observed for that element, so an element survives if any
|
|
236
|
+
* concurrent add was not seen by the remove — re-adding works, and a
|
|
237
|
+
* concurrent add-vs-remove resolves add-wins. <code>tombstoneRetention</code>
|
|
238
|
+
* optionally caps the tombstone set to bound memory against a remove flood; it
|
|
239
|
+
* drops the oldest tombstones, which can resurrect a concurrently-removed
|
|
240
|
+
* element, so leave it unset unless that trade-off is acceptable.
|
|
241
|
+
*
|
|
242
|
+
* Each add stamps a unique tag; remove tombstones the tags currently observed
|
|
243
|
+
* for that element. An element is present if it has a live (un-tombstoned) tag.
|
|
244
|
+
*
|
|
245
|
+
* @opts
|
|
246
|
+
* replicaId: string, // this replica's id (default: random)
|
|
247
|
+
* tombstoneRetention: number, // optional cap on retained tombstones (default: unbounded)
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* var a = b.crdt.orSet().add("x");
|
|
251
|
+
* var c = b.crdt.orSet.fromState(a.state()).add("x"); // re-add elsewhere
|
|
252
|
+
* a.remove("x");
|
|
253
|
+
* a.merge(c).value(); // → ["x"] (concurrent re-add survives)
|
|
254
|
+
*/
|
|
255
|
+
function orSet(opts) {
|
|
256
|
+
opts = opts || {};
|
|
257
|
+
var replicaId = _replicaId(opts);
|
|
258
|
+
var tombstoneRetention = opts.tombstoneRetention;
|
|
259
|
+
if (tombstoneRetention != null) _posInt(tombstoneRetention, "tombstoneRetention");
|
|
260
|
+
// elems: key -> array of tags ; tombstones: Set of removed tags
|
|
261
|
+
var elems = {};
|
|
262
|
+
if (opts._elems) { for (var k in opts._elems) if (Object.prototype.hasOwnProperty.call(opts._elems, k)) elems[k] = opts._elems[k].slice(); }
|
|
263
|
+
var tombstones = new Set(opts._tombstones || []);
|
|
264
|
+
var seq = 0;
|
|
265
|
+
|
|
266
|
+
function _tag() { return replicaId + ":" + (++seq) + ":" + bCrypto.generateToken(4); }
|
|
267
|
+
function _liveTags(key) { return (elems[key] || []).filter(function (t) { return !tombstones.has(t); }); }
|
|
268
|
+
function _gcTombstones() {
|
|
269
|
+
if (tombstoneRetention == null || tombstones.size <= tombstoneRetention) return;
|
|
270
|
+
// Bounded-memory tradeoff (opt-in): drop oldest tombstones. Documented to
|
|
271
|
+
// possibly resurrect a concurrently-removed element — full causal GC ships
|
|
272
|
+
// with the replicator slice.
|
|
273
|
+
var arr = Array.from(tombstones);
|
|
274
|
+
tombstones = new Set(arr.slice(arr.length - tombstoneRetention));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
type: "orSet",
|
|
279
|
+
replicaId: replicaId,
|
|
280
|
+
add: function (x) { var key = _key(x); (elems[key] = elems[key] || []).push(_tag()); return this; },
|
|
281
|
+
remove: function (x) { var key = _key(x); _liveTags(key).forEach(function (t) { tombstones.add(t); }); _gcTombstones(); return this; },
|
|
282
|
+
has: function (x) { return _liveTags(_key(x)).length > 0; },
|
|
283
|
+
value: function () { var live = []; for (var key in elems) if (Object.prototype.hasOwnProperty.call(elems, key) && _liveTags(key).length > 0) live.push(key); return _decodeKeys(new Set(live)); },
|
|
284
|
+
state: function () { var e = {}; for (var key in elems) if (Object.prototype.hasOwnProperty.call(elems, key)) e[key] = elems[key].slice(); return { type: "orSet", elems: e, tombstones: Array.from(tombstones) }; },
|
|
285
|
+
merge: function (other) {
|
|
286
|
+
var o = _otherState(other, "orSet");
|
|
287
|
+
var e = {};
|
|
288
|
+
var key;
|
|
289
|
+
for (key in elems) if (Object.prototype.hasOwnProperty.call(elems, key)) e[key] = elems[key].slice();
|
|
290
|
+
for (key in o.elems) if (Object.prototype.hasOwnProperty.call(o.elems, key)) {
|
|
291
|
+
var merged = (e[key] || []).concat(o.elems[key]);
|
|
292
|
+
e[key] = Array.from(new Set(merged)); // union of tags, dedup
|
|
293
|
+
}
|
|
294
|
+
var ts = new Set(tombstones);
|
|
295
|
+
o.tombstones.forEach(function (t) { ts.add(t); });
|
|
296
|
+
return orSet({ replicaId: replicaId, tombstoneRetention: tombstoneRetention, _elems: e, _tombstones: ts });
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
orSet.fromState = function (s, opts) { _assertState(s, "orSet"); return orSet(Object.assign({}, opts, { _elems: s.elems, _tombstones: s.tombstones })); };
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @primitive b.crdt.lwwRegister
|
|
304
|
+
* @signature b.crdt.lwwRegister(opts?)
|
|
305
|
+
* @since 0.13.4
|
|
306
|
+
* @status stable
|
|
307
|
+
* @compliance soc2
|
|
308
|
+
* @related b.crdt.pnCounter, b.crdt.orMap
|
|
309
|
+
*
|
|
310
|
+
* A last-write-wins register: holds a single value with a timestamp;
|
|
311
|
+
* <code>merge</code> keeps the higher-timestamped write, breaking ties by the
|
|
312
|
+
* higher <code>replicaId</code> so the outcome is deterministic. Pass an
|
|
313
|
+
* explicit timestamp to <code>set</code> for a logical clock, or omit it to
|
|
314
|
+
* use wall-clock milliseconds.
|
|
315
|
+
*
|
|
316
|
+
* @opts
|
|
317
|
+
* replicaId: string, // this replica's id (default: random)
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* var a = b.crdt.lwwRegister({ replicaId: "a" }).set("first", 1);
|
|
321
|
+
* var c = b.crdt.lwwRegister({ replicaId: "c" }).set("second", 2);
|
|
322
|
+
* a.merge(c).value(); // → "second"
|
|
323
|
+
*/
|
|
324
|
+
function lwwRegister(opts) {
|
|
325
|
+
opts = opts || {};
|
|
326
|
+
var replicaId = _replicaId(opts);
|
|
327
|
+
var current = opts._current || { value: null, ts: -1, replicaId: "" };
|
|
328
|
+
|
|
329
|
+
function _beats(a, b) { return a.ts > b.ts || (a.ts === b.ts && a.replicaId > b.replicaId); }
|
|
330
|
+
return {
|
|
331
|
+
type: "lwwRegister",
|
|
332
|
+
replicaId: replicaId,
|
|
333
|
+
set: function (v, ts) { ts = ts == null ? Date.now() : _posInt(ts, "ts"); var cand = { value: v, ts: ts, replicaId: replicaId }; if (_beats(cand, current)) current = cand; return this; },
|
|
334
|
+
value: function () { return current.value; },
|
|
335
|
+
timestamp: function () { return current.ts; },
|
|
336
|
+
state: function () { return { type: "lwwRegister", current: { value: current.value, ts: current.ts, replicaId: current.replicaId } }; },
|
|
337
|
+
merge: function (other) { var o = _otherState(other, "lwwRegister"); var win = _beats(o.current, current) ? o.current : current; return lwwRegister({ replicaId: replicaId, _current: { value: win.value, ts: win.ts, replicaId: win.replicaId } }); },
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
lwwRegister.fromState = function (s, opts) { _assertState(s, "lwwRegister"); return lwwRegister(Object.assign({}, opts, { _current: s.current })); };
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @primitive b.crdt.orMap
|
|
344
|
+
* @signature b.crdt.orMap(opts?)
|
|
345
|
+
* @since 0.13.4
|
|
346
|
+
* @status stable
|
|
347
|
+
* @compliance soc2
|
|
348
|
+
* @related b.crdt.orSet, b.crdt.lwwRegister
|
|
349
|
+
*
|
|
350
|
+
* An observed-remove map: key presence follows observed-remove-set semantics
|
|
351
|
+
* (a key can be set, removed, and set again), and each key's value is a
|
|
352
|
+
* last-write-wins register, so concurrent writes to a live key converge by
|
|
353
|
+
* timestamp (higher wins, ties by replicaId). Removing a key clears its value
|
|
354
|
+
* register locally, so a re-add on the same replica starts clean; across
|
|
355
|
+
* replicas the value is strictly last-write-wins by timestamp — supply
|
|
356
|
+
* monotonic timestamps (the default wall-clock does) for re-add to win. Keys
|
|
357
|
+
* are non-empty strings.
|
|
358
|
+
*
|
|
359
|
+
* Keys follow OR-Set add/remove semantics; each key's value is an LWW register,
|
|
360
|
+
* so concurrent writes to the same key converge by last-write-wins.
|
|
361
|
+
*
|
|
362
|
+
* @opts
|
|
363
|
+
* replicaId: string, // this replica's id (default: random)
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* var a = b.crdt.orMap({ replicaId: "a" }).set("k", "v1", 1);
|
|
367
|
+
* var c = b.crdt.orMap({ replicaId: "c" }).set("k", "v2", 2);
|
|
368
|
+
* a.merge(c).value(); // → { k: "v2" }
|
|
369
|
+
*/
|
|
370
|
+
function orMap(opts) {
|
|
371
|
+
opts = opts || {};
|
|
372
|
+
var replicaId = _replicaId(opts);
|
|
373
|
+
var keys = orSet({ replicaId: replicaId, _elems: opts._keyElems, _tombstones: opts._keyTombstones });
|
|
374
|
+
var vals = {}; // key -> lwwRegister state
|
|
375
|
+
if (opts._vals) { for (var k in opts._vals) if (Object.prototype.hasOwnProperty.call(opts._vals, k)) vals[k] = opts._vals[k]; }
|
|
376
|
+
|
|
377
|
+
function _reg(key) { return lwwRegister.fromState(vals[key] || { type: "lwwRegister", current: { value: null, ts: -1, replicaId: "" } }, { replicaId: replicaId }); }
|
|
378
|
+
// Keys pass to the OR-Set raw (it encodes internally) and index `vals`
|
|
379
|
+
// directly, so the materialized value is keyed by the plain string.
|
|
380
|
+
return {
|
|
381
|
+
type: "orMap",
|
|
382
|
+
replicaId: replicaId,
|
|
383
|
+
set: function (key, v, ts) { key = _mapKey(key); keys.add(key); vals[key] = _reg(key).set(v, ts).state(); return this; },
|
|
384
|
+
// Removing a key clears its value register, so re-adding the key on this
|
|
385
|
+
// replica starts from a clean last-write-wins state rather than reusing the
|
|
386
|
+
// pre-remove value. (Across replicas the value still follows last-write-wins
|
|
387
|
+
// by timestamp — a concurrent, un-removed higher-timestamped write wins;
|
|
388
|
+
// making a re-add causally supersede needs vector clocks, which ship with
|
|
389
|
+
// the replicator slice.)
|
|
390
|
+
remove: function (key) { key = _mapKey(key); keys.remove(key); delete vals[key]; return this; },
|
|
391
|
+
has: function (key) { return keys.has(_mapKey(key)); },
|
|
392
|
+
get: function (key) { key = _mapKey(key); return keys.has(key) ? _reg(key).value() : undefined; },
|
|
393
|
+
value: function () { var out = {}; keys.value().forEach(function (key) { out[key] = _reg(key).value(); }); return out; },
|
|
394
|
+
state: function () { var ks = keys.state(); return { type: "orMap", keyElems: ks.elems, keyTombstones: ks.tombstones, vals: Object.assign({}, vals) }; },
|
|
395
|
+
merge: function (other) {
|
|
396
|
+
var o = _otherState(other, "orMap");
|
|
397
|
+
var mergedKeys = keys.merge({ state: function () { return { type: "orSet", elems: o.keyElems, tombstones: o.keyTombstones }; } }).state();
|
|
398
|
+
var v = {};
|
|
399
|
+
var key;
|
|
400
|
+
for (key in vals) if (Object.prototype.hasOwnProperty.call(vals, key)) v[key] = vals[key];
|
|
401
|
+
for (key in o.vals) if (Object.prototype.hasOwnProperty.call(o.vals, key)) {
|
|
402
|
+
if (!v[key]) { v[key] = o.vals[key]; }
|
|
403
|
+
else {
|
|
404
|
+
var merged = lwwRegister.fromState(v[key], { replicaId: replicaId }).merge({ state: function () { return o.vals[key]; } });
|
|
405
|
+
v[key] = merged.state();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return orMap({ replicaId: replicaId, _keyElems: mergedKeys.elems, _keyTombstones: mergedKeys.tombstones, _vals: v });
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
orMap.fromState = function (s, opts) { _assertState(s, "orMap"); return orMap(Object.assign({}, opts, { _keyElems: s.keyElems, _keyTombstones: s.keyTombstones, _vals: s.vals })); };
|
|
413
|
+
|
|
414
|
+
// ---- shared helpers --------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
// Set/map elements are keyed by a reversible string so structured values work.
|
|
417
|
+
function _key(x) {
|
|
418
|
+
if (typeof x === "string") return "s:" + x;
|
|
419
|
+
return "j:" + JSON.stringify(x);
|
|
420
|
+
}
|
|
421
|
+
function _mapKey(k) {
|
|
422
|
+
if (typeof k !== "string" || k.length === 0) throw new CrdtError("crdt/bad-key", "crdt.orMap: keys must be non-empty strings");
|
|
423
|
+
return k;
|
|
424
|
+
}
|
|
425
|
+
function _decodeKeys(set) {
|
|
426
|
+
// Sort by the encoded key (a deterministic, unique string for every element,
|
|
427
|
+
// including structured ones) BEFORE decoding, so the materialized array order
|
|
428
|
+
// is identical regardless of merge order — structured elements would all
|
|
429
|
+
// collapse to "[object Object]" if sorted by their decoded value.
|
|
430
|
+
return Array.from(set).sort().map(function (k) {
|
|
431
|
+
return k.charAt(0) === "s" ? k.slice(2) : safeJson.parse(k.slice(2));
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
function _otherState(other, expected) {
|
|
435
|
+
var s = other && typeof other.state === "function" ? other.state() : other;
|
|
436
|
+
_assertState(s, expected);
|
|
437
|
+
return s;
|
|
438
|
+
}
|
|
439
|
+
function _assertState(s, expected) {
|
|
440
|
+
if (!s || typeof s !== "object") throw new CrdtError("crdt/bad-state", "crdt: expected a " + expected + " state object");
|
|
441
|
+
if (s.type !== expected) throw new CrdtError("crdt/type-mismatch", "crdt: expected a " + expected + " state, got '" + s.type + "'");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
module.exports = {
|
|
445
|
+
gCounter: gCounter,
|
|
446
|
+
pnCounter: pnCounter,
|
|
447
|
+
gSet: gSet,
|
|
448
|
+
twoPSet: twoPSet,
|
|
449
|
+
orSet: orSet,
|
|
450
|
+
lwwRegister: lwwRegister,
|
|
451
|
+
orMap: orMap,
|
|
452
|
+
CrdtError: CrdtError,
|
|
453
|
+
};
|