antietcd 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +501 -0
- package/anticli.js +263 -0
- package/anticluster.js +526 -0
- package/antietcd-app.js +122 -0
- package/antietcd.d.ts +155 -0
- package/antietcd.js +552 -0
- package/antipersistence.js +138 -0
- package/common.js +38 -0
- package/etctree.js +875 -0
- package/package.json +55 -0
- package/stable-stringify.js +78 -0
package/etctree.js
ADDED
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
// Simple in-memory etcd-like key/value store
|
|
2
|
+
// (c) Vitaliy Filippov, 2024
|
|
3
|
+
// License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1
|
|
4
|
+
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const stableStringify = require('./stable-stringify.js');
|
|
7
|
+
const { RequestError } = require('./common.js');
|
|
8
|
+
|
|
9
|
+
/*type TreeNode = {
|
|
10
|
+
value?: any,
|
|
11
|
+
create_revision?: number,
|
|
12
|
+
mod_revision?: number,
|
|
13
|
+
version?: number,
|
|
14
|
+
lease?: string,
|
|
15
|
+
children: { [string]: TreeNode },
|
|
16
|
+
watchers?: number[],
|
|
17
|
+
key_watchers?: number[],
|
|
18
|
+
};*/
|
|
19
|
+
|
|
20
|
+
class EtcTree
|
|
21
|
+
{
|
|
22
|
+
constructor(use_base64)
|
|
23
|
+
{
|
|
24
|
+
this.state = {};
|
|
25
|
+
this.leases = {};
|
|
26
|
+
this.watchers = {};
|
|
27
|
+
this.watcher_id = 0;
|
|
28
|
+
this.mod_revision = 0;
|
|
29
|
+
this.compact_revision = 0;
|
|
30
|
+
this.use_base64 = use_base64;
|
|
31
|
+
this.replicate = null;
|
|
32
|
+
this.paused = false;
|
|
33
|
+
this.active_immediate = [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
destroy()
|
|
37
|
+
{
|
|
38
|
+
this.pause_leases();
|
|
39
|
+
for (const imm of this.active_immediate)
|
|
40
|
+
{
|
|
41
|
+
clearImmediate(imm);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
set_replicate_watcher(replicate)
|
|
46
|
+
{
|
|
47
|
+
// Replication watcher is special:
|
|
48
|
+
// It should be an async function and it is called BEFORE notifying all
|
|
49
|
+
// other watchers about any change.
|
|
50
|
+
// It may also throw to prevent notifying at all if replication fails.
|
|
51
|
+
this.replicate = replicate;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
de64(k)
|
|
55
|
+
{
|
|
56
|
+
if (k == null) // null or undefined
|
|
57
|
+
return k;
|
|
58
|
+
return this.use_base64 ? Buffer.from(k, 'base64').toString() : k;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
b64(k)
|
|
62
|
+
{
|
|
63
|
+
if (k == null) // null or undefined
|
|
64
|
+
return k;
|
|
65
|
+
return this.use_base64 ? Buffer.from(k).toString('base64') : k;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_check(chk)
|
|
69
|
+
{
|
|
70
|
+
const parts = this._key_parts(this.de64(chk.key));
|
|
71
|
+
const { cur } = this._get_subtree(parts, false, false);
|
|
72
|
+
let check_value, ref_value;
|
|
73
|
+
if (chk.target === 'MOD')
|
|
74
|
+
{
|
|
75
|
+
check_value = cur && cur.mod_revision || 0;
|
|
76
|
+
ref_value = chk.mod_revision || 0;
|
|
77
|
+
}
|
|
78
|
+
else if (chk.target === 'CREATE')
|
|
79
|
+
{
|
|
80
|
+
check_value = cur && cur.create_revision || 0;
|
|
81
|
+
ref_value = chk.create_revision || 0;
|
|
82
|
+
}
|
|
83
|
+
else if (chk.target === 'VERSION')
|
|
84
|
+
{
|
|
85
|
+
check_value = cur && cur.version || 0;
|
|
86
|
+
ref_value = chk.version || 0;
|
|
87
|
+
}
|
|
88
|
+
else if (chk.target === 'LEASE')
|
|
89
|
+
{
|
|
90
|
+
check_value = cur && cur.lease;
|
|
91
|
+
ref_value = chk.lease;
|
|
92
|
+
}
|
|
93
|
+
else if (chk.target === 'VALUE')
|
|
94
|
+
{
|
|
95
|
+
check_value = cur && cur.value;
|
|
96
|
+
ref_value = chk.value;
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
{
|
|
100
|
+
throw new RequestError(501, 'Unsupported comparison target: '+chk.target);
|
|
101
|
+
}
|
|
102
|
+
if (chk.result === 'LESS')
|
|
103
|
+
{
|
|
104
|
+
return check_value < ref_value;
|
|
105
|
+
}
|
|
106
|
+
else if (chk.result)
|
|
107
|
+
{
|
|
108
|
+
throw new RequestError(501, 'Unsupported comparison result: '+chk.result);
|
|
109
|
+
}
|
|
110
|
+
return check_value == ref_value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_key_parts(key)
|
|
114
|
+
{
|
|
115
|
+
const parts = key.replace(/\/\/+/g, '/').replace(/\/$/g, ''); // trim beginning?
|
|
116
|
+
return parts === '' ? [] : parts.split('/');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_get_range(req)
|
|
120
|
+
{
|
|
121
|
+
const key = this.de64(req.key);
|
|
122
|
+
const end = this.de64(req.range_end);
|
|
123
|
+
if (end != null && (key[key.length-1] != '/' || end[end.length-1] != '0' ||
|
|
124
|
+
end.substr(0, end.length-1) !== key.substr(0, key.length-1)))
|
|
125
|
+
{
|
|
126
|
+
throw new RequestError(501, 'Non-directory range queries are unsupported');
|
|
127
|
+
}
|
|
128
|
+
const parts = this._key_parts(key);
|
|
129
|
+
return { parts, all: end != null };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_get_subtree(parts, create, notify)
|
|
133
|
+
{
|
|
134
|
+
let cur = this.state;
|
|
135
|
+
let watchers = notify ? [] : null;
|
|
136
|
+
for (let k of parts)
|
|
137
|
+
{
|
|
138
|
+
if (notify && cur.watchers)
|
|
139
|
+
{
|
|
140
|
+
watchers.push.apply(watchers, cur.watchers);
|
|
141
|
+
}
|
|
142
|
+
if (!cur.children)
|
|
143
|
+
{
|
|
144
|
+
if (!create)
|
|
145
|
+
{
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
cur.children = {};
|
|
149
|
+
}
|
|
150
|
+
if (!cur.children[k])
|
|
151
|
+
{
|
|
152
|
+
if (!create)
|
|
153
|
+
{
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
cur.children[k] = {};
|
|
157
|
+
}
|
|
158
|
+
cur = cur.children[k];
|
|
159
|
+
}
|
|
160
|
+
if (notify && cur.watchers)
|
|
161
|
+
{
|
|
162
|
+
watchers.push.apply(watchers, cur.watchers);
|
|
163
|
+
}
|
|
164
|
+
return { watchers, cur };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// create a snapshot of all data including leases
|
|
168
|
+
dump(persistent_only, value_filter)
|
|
169
|
+
{
|
|
170
|
+
const snapshot = {
|
|
171
|
+
state: this._copy_tree(this.state, persistent_only, value_filter) || {},
|
|
172
|
+
mod_revision: this.mod_revision,
|
|
173
|
+
compact_revision: this.compact_revision,
|
|
174
|
+
};
|
|
175
|
+
if (!persistent_only)
|
|
176
|
+
{
|
|
177
|
+
snapshot.leases = {};
|
|
178
|
+
for (const id in this.leases)
|
|
179
|
+
{
|
|
180
|
+
const lease = this.leases[id];
|
|
181
|
+
snapshot.leases[id] = { ttl: lease.ttl, expires: lease.expires };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return snapshot;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_copy_tree(cur, no_lease, value_filter)
|
|
188
|
+
{
|
|
189
|
+
let nonempty = cur.value != null && (!no_lease || !cur.lease);
|
|
190
|
+
let filtered;
|
|
191
|
+
if (nonempty && value_filter)
|
|
192
|
+
{
|
|
193
|
+
filtered = value_filter(cur.value);
|
|
194
|
+
nonempty = nonempty && filtered != null;
|
|
195
|
+
}
|
|
196
|
+
const copy = (nonempty ? { ...cur } : {});
|
|
197
|
+
copy.children = {};
|
|
198
|
+
if (nonempty && value_filter)
|
|
199
|
+
{
|
|
200
|
+
copy.value = filtered;
|
|
201
|
+
}
|
|
202
|
+
delete copy.watchers;
|
|
203
|
+
delete copy.key_watchers;
|
|
204
|
+
let has_children = false;
|
|
205
|
+
for (const k in cur.children)
|
|
206
|
+
{
|
|
207
|
+
const child = this._copy_tree(cur.children[k], no_lease, value_filter);
|
|
208
|
+
if (child)
|
|
209
|
+
{
|
|
210
|
+
copy.children[k] = child;
|
|
211
|
+
has_children = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!nonempty && !has_children)
|
|
215
|
+
{
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
if (!has_children)
|
|
219
|
+
{
|
|
220
|
+
delete copy.children;
|
|
221
|
+
}
|
|
222
|
+
return copy;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// load snapshot of all data including leases
|
|
226
|
+
load(snapshot, update_only)
|
|
227
|
+
{
|
|
228
|
+
if (!update_only || this.mod_revision < snapshot.mod_revision)
|
|
229
|
+
{
|
|
230
|
+
this.mod_revision = snapshot.mod_revision;
|
|
231
|
+
}
|
|
232
|
+
if (!update_only || this.compact_revision > (snapshot.compact_revision||0))
|
|
233
|
+
{
|
|
234
|
+
this.compact_revision = snapshot.compact_revision||0;
|
|
235
|
+
}
|
|
236
|
+
// First apply leases
|
|
237
|
+
const notifications = [];
|
|
238
|
+
if (!update_only && snapshot.leases)
|
|
239
|
+
{
|
|
240
|
+
for (const id in this.leases)
|
|
241
|
+
{
|
|
242
|
+
if (!snapshot.leases[id])
|
|
243
|
+
{
|
|
244
|
+
// Revoke without replicating and notifying
|
|
245
|
+
this._sync_revoke_lease(id, notifications, this.mod_revision);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const id in snapshot.leases||{})
|
|
250
|
+
{
|
|
251
|
+
this.load_lease({ id, ...snapshot.leases[id] });
|
|
252
|
+
}
|
|
253
|
+
// Then find and apply the difference in data
|
|
254
|
+
this._restore_diff(update_only, this.state, snapshot.state, null, this.state.watchers || [], notifications);
|
|
255
|
+
this._notify(notifications);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
_restore_diff(update_only, cur_old, cur_new, prefix, watchers, notifications)
|
|
259
|
+
{
|
|
260
|
+
if (!update_only || !cur_old.mod_revision || cur_old.mod_revision < cur_new.mod_revision)
|
|
261
|
+
{
|
|
262
|
+
const key = prefix === null ? '' : prefix;
|
|
263
|
+
if (!eq(cur_old.lease, cur_new.lease))
|
|
264
|
+
{
|
|
265
|
+
if (cur_old.lease && this.leases[cur_old.lease])
|
|
266
|
+
{
|
|
267
|
+
delete this.leases[cur_old.lease].keys[key];
|
|
268
|
+
}
|
|
269
|
+
cur_old.lease = cur_new.lease;
|
|
270
|
+
if (cur_new.lease && this.leases[cur_new.lease])
|
|
271
|
+
{
|
|
272
|
+
this.leases[cur_new.lease].keys[key] = true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
cur_old.mod_revision = cur_new.mod_revision;
|
|
276
|
+
cur_old.create_revision = cur_new.create_revision;
|
|
277
|
+
cur_old.version = cur_new.version;
|
|
278
|
+
if (!eq(cur_old.value, cur_new.value))
|
|
279
|
+
{
|
|
280
|
+
cur_old.value = cur_new.value;
|
|
281
|
+
const key_watchers = (cur_old.key_watchers ? [ ...watchers, ...(cur_old.key_watchers||[]) ] : watchers);
|
|
282
|
+
const notify = { watchers: key_watchers, key, value: cur_new.value, mod_revision: cur_new.mod_revision };
|
|
283
|
+
if (cur_new.lease)
|
|
284
|
+
{
|
|
285
|
+
notify.lease = cur_new.lease;
|
|
286
|
+
}
|
|
287
|
+
notifications.push(notify);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
cur_old.children = cur_old.children || {};
|
|
291
|
+
for (const k in cur_new.children)
|
|
292
|
+
{
|
|
293
|
+
if (!cur_old.children[k])
|
|
294
|
+
{
|
|
295
|
+
cur_old.children[k] = cur_new.children[k];
|
|
296
|
+
}
|
|
297
|
+
else
|
|
298
|
+
{
|
|
299
|
+
this._restore_diff(
|
|
300
|
+
update_only, cur_old.children[k], cur_new.children[k],
|
|
301
|
+
prefix === null ? k : prefix+'/'+k,
|
|
302
|
+
cur_old.children[k].watchers ? [ ...watchers, ...cur_old.children[k].watchers ] : watchers,
|
|
303
|
+
notifications
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (!update_only)
|
|
308
|
+
{
|
|
309
|
+
for (const k in cur_old.children)
|
|
310
|
+
{
|
|
311
|
+
if (!cur_new.children || !cur_new.children[k])
|
|
312
|
+
{
|
|
313
|
+
// Delete subtree
|
|
314
|
+
this._delete_all(
|
|
315
|
+
notifications,
|
|
316
|
+
cur_old.children[k].watchers ? [ ...watchers, ...cur_old.children[k].watchers ] : watchers,
|
|
317
|
+
cur_old.children[k], true,
|
|
318
|
+
prefix === null ? k : prefix+'/'+k,
|
|
319
|
+
this.mod_revision
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// slave/follower nodes don't expire leases themselves, they listen for the leader instead
|
|
327
|
+
pause_leases()
|
|
328
|
+
{
|
|
329
|
+
if (this.paused)
|
|
330
|
+
{
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
this.paused = true;
|
|
334
|
+
for (const id in this.leases)
|
|
335
|
+
{
|
|
336
|
+
const lease = this.leases[id];
|
|
337
|
+
if (lease.timer_id)
|
|
338
|
+
{
|
|
339
|
+
clearTimeout(lease.timer_id);
|
|
340
|
+
lease.timer_id = null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
resume_leases()
|
|
346
|
+
{
|
|
347
|
+
if (!this.paused)
|
|
348
|
+
{
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
this.paused = false;
|
|
352
|
+
for (const id in this.leases)
|
|
353
|
+
{
|
|
354
|
+
this._set_expire(id);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
_set_expire(id)
|
|
359
|
+
{
|
|
360
|
+
if (!this.paused)
|
|
361
|
+
{
|
|
362
|
+
const lease = this.leases[id];
|
|
363
|
+
if (!lease.timer_id)
|
|
364
|
+
{
|
|
365
|
+
lease.timer_id = setTimeout(() => this.api_revoke_lease({ ID: id }).catch(console.error), lease.expires - Date.now());
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async api_grant_lease(req)
|
|
371
|
+
{
|
|
372
|
+
let id;
|
|
373
|
+
while (!id || this.leases[id])
|
|
374
|
+
{
|
|
375
|
+
id = crypto.randomBytes(8).toString('hex');
|
|
376
|
+
}
|
|
377
|
+
const expires = Date.now() + req.TTL*1000;
|
|
378
|
+
this.leases[id] = { ttl: req.TTL, expires, timer_id: null, keys: {} };
|
|
379
|
+
this.mod_revision++;
|
|
380
|
+
this._set_expire(id);
|
|
381
|
+
if (this.replicate)
|
|
382
|
+
{
|
|
383
|
+
await this.replicate({ header: { revision: this.mod_revision }, leases: [ { id, ttl: req.TTL, expires } ] });
|
|
384
|
+
}
|
|
385
|
+
return { header: { revision: this.mod_revision }, ID: id, TTL: req.TTL };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async api_keepalive_lease(req)
|
|
389
|
+
{
|
|
390
|
+
const id = req.ID;
|
|
391
|
+
if (!this.leases[id])
|
|
392
|
+
{
|
|
393
|
+
throw new RequestError(400, 'unknown lease');
|
|
394
|
+
}
|
|
395
|
+
const lease = this.leases[id];
|
|
396
|
+
if (lease.timer_id)
|
|
397
|
+
{
|
|
398
|
+
clearTimeout(lease.timer_id);
|
|
399
|
+
lease.timer_id = null;
|
|
400
|
+
}
|
|
401
|
+
const ttl = this.leases[id].ttl;
|
|
402
|
+
lease.expires = Date.now() + ttl*1000;
|
|
403
|
+
this.mod_revision++;
|
|
404
|
+
this._set_expire(id);
|
|
405
|
+
if (this.replicate)
|
|
406
|
+
{
|
|
407
|
+
await this.replicate({ header: { revision: this.mod_revision }, leases: [ { id, ttl, expires: lease.expires } ] });
|
|
408
|
+
}
|
|
409
|
+
// extra wrapping in { result: ... }
|
|
410
|
+
return { result: { header: { revision: this.mod_revision }, ID: id, TTL: ''+ttl } };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
load_lease(lease)
|
|
414
|
+
{
|
|
415
|
+
const id = lease.id;
|
|
416
|
+
if (!this.leases[id])
|
|
417
|
+
{
|
|
418
|
+
this.leases[id] = { ...lease, timer_id: null, keys: {} };
|
|
419
|
+
}
|
|
420
|
+
else if (this.leases[id].ttl != lease.ttl ||
|
|
421
|
+
this.leases[id].expires != lease.expires)
|
|
422
|
+
{
|
|
423
|
+
this.leases[id].ttl = lease.ttl;
|
|
424
|
+
this.leases[id].expires = lease.expires;
|
|
425
|
+
}
|
|
426
|
+
else
|
|
427
|
+
{
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
if (this.leases[id].timer_id)
|
|
431
|
+
{
|
|
432
|
+
clearTimeout(this.leases[id].timer_id);
|
|
433
|
+
this.leases[id].timer_id = null;
|
|
434
|
+
}
|
|
435
|
+
this._set_expire(id);
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
_sync_revoke_lease(id, notifications, next_revision)
|
|
440
|
+
{
|
|
441
|
+
if (!this.leases[id])
|
|
442
|
+
{
|
|
443
|
+
throw new RequestError(400, 'unknown lease');
|
|
444
|
+
}
|
|
445
|
+
for (const key in this.leases[id].keys)
|
|
446
|
+
{
|
|
447
|
+
this._delete_range({ key }, next_revision, notifications);
|
|
448
|
+
}
|
|
449
|
+
delete this.leases[id];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async api_revoke_lease(req, no_throw)
|
|
453
|
+
{
|
|
454
|
+
const notifications = [];
|
|
455
|
+
if (!this.leases[req.ID])
|
|
456
|
+
{
|
|
457
|
+
if (no_throw)
|
|
458
|
+
return null;
|
|
459
|
+
throw new RequestError(400, 'unknown lease');
|
|
460
|
+
}
|
|
461
|
+
this.mod_revision++;
|
|
462
|
+
this._sync_revoke_lease(req.ID, notifications, this.mod_revision);
|
|
463
|
+
if (this.replicate)
|
|
464
|
+
{
|
|
465
|
+
await this.notify_replicator(notifications, [ { id: req.ID } ]);
|
|
466
|
+
}
|
|
467
|
+
this._notify(notifications);
|
|
468
|
+
return { header: { revision: this.mod_revision } };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async notify_replicator(notifications, leases)
|
|
472
|
+
{
|
|
473
|
+
// First replicate the change and then notify watchers about it
|
|
474
|
+
const all_changes = {};
|
|
475
|
+
for (const chg of notifications)
|
|
476
|
+
{
|
|
477
|
+
all_changes[chg.key] = { ...chg };
|
|
478
|
+
delete all_changes[chg.key].watchers;
|
|
479
|
+
}
|
|
480
|
+
await this.replicate({ header: { revision: this.mod_revision }, events: Object.values(all_changes), leases });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async apply_replication(msg)
|
|
484
|
+
{
|
|
485
|
+
this.mod_revision = msg.header.revision;
|
|
486
|
+
const notifications = [];
|
|
487
|
+
if ((msg.leases||[]).length)
|
|
488
|
+
{
|
|
489
|
+
for (const lease of msg.leases)
|
|
490
|
+
{
|
|
491
|
+
if (lease.ttl)
|
|
492
|
+
{
|
|
493
|
+
this.load_lease(lease);
|
|
494
|
+
}
|
|
495
|
+
else
|
|
496
|
+
{
|
|
497
|
+
this._sync_revoke_lease(lease.id, notifications, this.mod_revision);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if ((msg.events||[]).length)
|
|
502
|
+
{
|
|
503
|
+
for (const ev of msg.events)
|
|
504
|
+
{
|
|
505
|
+
if (ev.value == null)
|
|
506
|
+
{
|
|
507
|
+
this._delete_range({ key: ev.key }, ev.mod_revision, notifications);
|
|
508
|
+
}
|
|
509
|
+
else
|
|
510
|
+
{
|
|
511
|
+
this._put({ key: ev.key, value: ev.value, lease: ev.lease }, ev.mod_revision, notifications);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (this.replicate)
|
|
516
|
+
{
|
|
517
|
+
await this.notify_replicator(notifications, msg.leases);
|
|
518
|
+
}
|
|
519
|
+
this._notify(notifications);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// forget deletions before compact_revision
|
|
523
|
+
compact(compact_revision)
|
|
524
|
+
{
|
|
525
|
+
this._compact(compact_revision, this.state);
|
|
526
|
+
this.compact_revision = compact_revision;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
_compact(compact_revision, cur)
|
|
530
|
+
{
|
|
531
|
+
for (const key in cur.children||{})
|
|
532
|
+
{
|
|
533
|
+
const child = cur.children[key];
|
|
534
|
+
this._compact(compact_revision, child);
|
|
535
|
+
if (emptyObj(child.children) && child.value == null && child.mod_revision < compact_revision)
|
|
536
|
+
{
|
|
537
|
+
delete cur.children[key];
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
api_create_watch(req, send)
|
|
543
|
+
{
|
|
544
|
+
const { parts, all } = this._get_range(req);
|
|
545
|
+
if (req.start_revision && this.compact_revision && this.compact_revision > req.start_revision)
|
|
546
|
+
{
|
|
547
|
+
// Deletions up to this.compact_revision are forgotten
|
|
548
|
+
return { compact_revision: this.compact_revision };
|
|
549
|
+
}
|
|
550
|
+
let watch_id = req.watch_id;
|
|
551
|
+
if (watch_id instanceof Object)
|
|
552
|
+
{
|
|
553
|
+
throw new RequestError(400, 'invalid watch_id');
|
|
554
|
+
}
|
|
555
|
+
if (!watch_id)
|
|
556
|
+
{
|
|
557
|
+
watch_id = ++this.watcher_id;
|
|
558
|
+
}
|
|
559
|
+
if (!this.watchers[watch_id])
|
|
560
|
+
{
|
|
561
|
+
this.watchers[watch_id] = {
|
|
562
|
+
paths: [],
|
|
563
|
+
send,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
this.watchers[watch_id].paths.push(parts);
|
|
567
|
+
const { cur } = this._get_subtree(parts, true, false);
|
|
568
|
+
if (all)
|
|
569
|
+
{
|
|
570
|
+
cur.watchers = cur.watchers || [];
|
|
571
|
+
cur.watchers.push(watch_id);
|
|
572
|
+
}
|
|
573
|
+
else
|
|
574
|
+
{
|
|
575
|
+
cur.key_watchers = cur.key_watchers || [];
|
|
576
|
+
cur.key_watchers.push(watch_id);
|
|
577
|
+
}
|
|
578
|
+
if (req.start_revision && req.start_revision < this.mod_revision)
|
|
579
|
+
{
|
|
580
|
+
// Send initial changes
|
|
581
|
+
const imm = setImmediate(() =>
|
|
582
|
+
{
|
|
583
|
+
this.active_immediate = this.active_immediate.filter(i => i !== imm);
|
|
584
|
+
const events = [];
|
|
585
|
+
const { cur } = this._get_subtree([], false, false);
|
|
586
|
+
this._get_modified(events, cur, null, req.start_revision);
|
|
587
|
+
send({ result: { header: { revision: this.mod_revision }, events } });
|
|
588
|
+
});
|
|
589
|
+
this.active_immediate.push(imm);
|
|
590
|
+
}
|
|
591
|
+
return { watch_id, created: true };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
_get_modified(events, cur, prefix, min_rev)
|
|
595
|
+
{
|
|
596
|
+
if (cur.mod_revision >= min_rev)
|
|
597
|
+
{
|
|
598
|
+
const ev = {
|
|
599
|
+
type: cur.value == null ? 'DELETE' : 'PUT',
|
|
600
|
+
kv: cur.value == null ? { key: this.b64(prefix === null ? '' : prefix) } : {
|
|
601
|
+
key: this.b64(prefix),
|
|
602
|
+
value: this.b64(cur.value),
|
|
603
|
+
mod_revision: cur.mod_revision,
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
if (cur.lease)
|
|
607
|
+
{
|
|
608
|
+
ev.kv.lease = cur.lease;
|
|
609
|
+
}
|
|
610
|
+
events.push(ev);
|
|
611
|
+
}
|
|
612
|
+
if (cur.children)
|
|
613
|
+
{
|
|
614
|
+
for (const k in cur.children)
|
|
615
|
+
{
|
|
616
|
+
this._get_modified(events, cur.children[k], prefix === null ? k : prefix+'/'+k, min_rev);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
api_cancel_watch(watch_id)
|
|
622
|
+
{
|
|
623
|
+
if (this.watchers[watch_id])
|
|
624
|
+
{
|
|
625
|
+
for (const parts of this.watchers[watch_id].paths)
|
|
626
|
+
{
|
|
627
|
+
const { cur } = this._get_subtree(parts, false, false);
|
|
628
|
+
if (cur)
|
|
629
|
+
{
|
|
630
|
+
if (cur.watchers)
|
|
631
|
+
{
|
|
632
|
+
cur.watchers = cur.watchers.filter(id => id != watch_id);
|
|
633
|
+
if (!cur.watchers.length)
|
|
634
|
+
cur.watchers = null;
|
|
635
|
+
}
|
|
636
|
+
if (cur.key_watchers)
|
|
637
|
+
{
|
|
638
|
+
cur.key_watchers = cur.key_watchers.filter(id => id != watch_id);
|
|
639
|
+
if (!cur.key_watchers.length)
|
|
640
|
+
cur.key_watchers = null;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
delete this.watchers[watch_id];
|
|
645
|
+
}
|
|
646
|
+
return { canceled: true };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
_notify(notifications)
|
|
650
|
+
{
|
|
651
|
+
if (!notifications.length)
|
|
652
|
+
{
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const by_watcher = {};
|
|
656
|
+
for (const notif of notifications)
|
|
657
|
+
{
|
|
658
|
+
const watchers = notif.watchers;
|
|
659
|
+
delete notif.watchers;
|
|
660
|
+
const conv = { type: ('value' in notif) ? 'PUT' : 'DELETE', kv: notif };
|
|
661
|
+
for (const wid of watchers)
|
|
662
|
+
{
|
|
663
|
+
if (this.watchers[wid])
|
|
664
|
+
{
|
|
665
|
+
by_watcher[wid] = by_watcher[wid] || { header: { revision: this.mod_revision }, events: {} };
|
|
666
|
+
by_watcher[wid].events[notif.key] = conv;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
for (const wid in by_watcher)
|
|
671
|
+
{
|
|
672
|
+
by_watcher[wid].events = Object.values(by_watcher[wid].events);
|
|
673
|
+
this.watchers[wid].send({ result: by_watcher[wid] });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async api_txn({ compare, success, failure })
|
|
678
|
+
{
|
|
679
|
+
const failed = (compare || []).filter(chk => !this._check(chk)).length > 0;
|
|
680
|
+
const responses = [];
|
|
681
|
+
const notifications = [];
|
|
682
|
+
const next_revision = this.mod_revision + 1;
|
|
683
|
+
for (const req of (failed ? failure : success) || [])
|
|
684
|
+
{
|
|
685
|
+
responses.push(this._txn_action(req, next_revision, notifications));
|
|
686
|
+
}
|
|
687
|
+
if (this.replicate && notifications.length)
|
|
688
|
+
{
|
|
689
|
+
// First replicate the change and then notify watchers about it
|
|
690
|
+
await this.notify_replicator(notifications);
|
|
691
|
+
}
|
|
692
|
+
this._notify(notifications);
|
|
693
|
+
return { header: { revision: this.mod_revision }, succeeded: !failed, responses };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
_txn_action(req, cur_revision, notifications)
|
|
697
|
+
{
|
|
698
|
+
if (req.request_range || req.requestRange)
|
|
699
|
+
{
|
|
700
|
+
return { response_range: this._range(req.request_range || req.requestRange) };
|
|
701
|
+
}
|
|
702
|
+
else if (req.request_put || req.requestPut)
|
|
703
|
+
{
|
|
704
|
+
return { response_put: this._put(req.request_put || req.requestPut, cur_revision, notifications) };
|
|
705
|
+
}
|
|
706
|
+
else if (req.request_delete_range || req.requestDeleteRange)
|
|
707
|
+
{
|
|
708
|
+
return { response_delete_range: this._delete_range(req.request_delete_range || req.requestDeleteRange, cur_revision, notifications) };
|
|
709
|
+
}
|
|
710
|
+
return {};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
_range(request_range)
|
|
714
|
+
{
|
|
715
|
+
// FIXME: limit, revision(-), sort_order, sort_target, serializable(-),
|
|
716
|
+
// count_only, min_mod_revision, max_mod_revision, min_create_revision, max_create_revision
|
|
717
|
+
const { parts, all } = this._get_range(request_range);
|
|
718
|
+
const { cur } = this._get_subtree(parts, false, false);
|
|
719
|
+
const kvs = [];
|
|
720
|
+
if (cur)
|
|
721
|
+
{
|
|
722
|
+
this._get_all(kvs, cur, all, parts.join('/') || null, request_range);
|
|
723
|
+
}
|
|
724
|
+
return { kvs };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
_put(request_put, cur_revision, notifications)
|
|
728
|
+
{
|
|
729
|
+
// FIXME: prev_kv, ignore_value(?), ignore_lease(?)
|
|
730
|
+
const parts = this._key_parts(this.de64(request_put.key));
|
|
731
|
+
const key = parts.join('/');
|
|
732
|
+
const value = this.de64(request_put.value);
|
|
733
|
+
const { cur, watchers } = this._get_subtree(parts, true, true);
|
|
734
|
+
if (cur.key_watchers)
|
|
735
|
+
{
|
|
736
|
+
watchers.push.apply(watchers, cur.key_watchers);
|
|
737
|
+
}
|
|
738
|
+
if (!eq(cur.value, value) || cur.lease != request_put.lease)
|
|
739
|
+
{
|
|
740
|
+
if (cur.lease && this.leases[cur.lease])
|
|
741
|
+
{
|
|
742
|
+
delete this.leases[cur.lease].keys[key];
|
|
743
|
+
}
|
|
744
|
+
if (request_put.lease)
|
|
745
|
+
{
|
|
746
|
+
if (!this.leases[request_put.lease])
|
|
747
|
+
{
|
|
748
|
+
throw new RequestError(400, 'unknown lease: '+request_put.lease);
|
|
749
|
+
}
|
|
750
|
+
cur.lease = request_put.lease;
|
|
751
|
+
this.leases[request_put.lease].keys[key] = true;
|
|
752
|
+
}
|
|
753
|
+
else if (cur.lease)
|
|
754
|
+
{
|
|
755
|
+
cur.lease = null;
|
|
756
|
+
}
|
|
757
|
+
this.mod_revision = cur_revision;
|
|
758
|
+
cur.version = (cur.version||0) + 1;
|
|
759
|
+
cur.mod_revision = cur_revision;
|
|
760
|
+
if (cur.value == null)
|
|
761
|
+
{
|
|
762
|
+
cur.create_revision = cur_revision;
|
|
763
|
+
}
|
|
764
|
+
cur.value = value;
|
|
765
|
+
const notify = { watchers, key: this.b64(key), value: this.b64(value), mod_revision: cur.mod_revision };
|
|
766
|
+
if (cur.lease)
|
|
767
|
+
{
|
|
768
|
+
notify.lease = cur.lease;
|
|
769
|
+
}
|
|
770
|
+
notifications.push(notify);
|
|
771
|
+
}
|
|
772
|
+
return {};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
_delete_range(request_delete_range, cur_revision, notifications)
|
|
776
|
+
{
|
|
777
|
+
// FIXME: prev_kv
|
|
778
|
+
const { parts, all } = this._get_range(request_delete_range);
|
|
779
|
+
const { cur, watchers } = this._get_subtree(parts, false, true);
|
|
780
|
+
const prevcount = notifications.length;
|
|
781
|
+
if (cur)
|
|
782
|
+
{
|
|
783
|
+
this._delete_all(notifications, watchers, cur, all, parts.join('/') || null, cur_revision);
|
|
784
|
+
}
|
|
785
|
+
return { deleted: notifications.length-prevcount };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
_get_all(kvs, cur, all, prefix, req)
|
|
789
|
+
{
|
|
790
|
+
if (req.limit && kvs.length > req.limit)
|
|
791
|
+
{
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (cur.value != null)
|
|
795
|
+
{
|
|
796
|
+
const item = { key: this.b64(prefix === null ? '' : prefix) };
|
|
797
|
+
if (!req.keys_only)
|
|
798
|
+
{
|
|
799
|
+
item.value = this.b64(cur.value);
|
|
800
|
+
item.mod_revision = cur.mod_revision;
|
|
801
|
+
//item.create_revision = cur.create_revision;
|
|
802
|
+
//item.version = cur.version;
|
|
803
|
+
if (cur.lease)
|
|
804
|
+
{
|
|
805
|
+
item.lease = cur.lease;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
kvs.push(item);
|
|
809
|
+
}
|
|
810
|
+
if (all && cur.children)
|
|
811
|
+
{
|
|
812
|
+
for (let k in cur.children)
|
|
813
|
+
{
|
|
814
|
+
this._get_all(kvs, cur.children[k], true, prefix === null ? k : prefix+'/'+k, req);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
_delete_all(notifications, watchers, cur, all, prefix, cur_revision)
|
|
820
|
+
{
|
|
821
|
+
if (cur.value != null)
|
|
822
|
+
{
|
|
823
|
+
// Do not actually forget the key until the deletion is confirmed by all replicas
|
|
824
|
+
// ...and until it's not required by watchers
|
|
825
|
+
if (cur.lease && this.leases[cur.lease])
|
|
826
|
+
{
|
|
827
|
+
delete this.leases[cur.lease].keys[prefix === null ? '' : prefix];
|
|
828
|
+
}
|
|
829
|
+
cur.value = null;
|
|
830
|
+
cur.version = 0;
|
|
831
|
+
cur.create_revision = null;
|
|
832
|
+
cur.mod_revision = cur_revision;
|
|
833
|
+
this.mod_revision = cur_revision;
|
|
834
|
+
notifications.push({
|
|
835
|
+
watchers: cur.key_watchers ? [ ...watchers, ...cur.key_watchers ] : watchers,
|
|
836
|
+
key: this.b64(prefix === null ? '' : prefix),
|
|
837
|
+
mod_revision: cur_revision,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
if (all && cur.children)
|
|
841
|
+
{
|
|
842
|
+
for (let k in cur.children)
|
|
843
|
+
{
|
|
844
|
+
const subw = cur.children[k].watchers ? [ ...watchers, ...cur.children[k].watchers ] : watchers;
|
|
845
|
+
this._delete_all(notifications, subw, cur.children[k], true, prefix === null ? k : prefix+'/'+k, cur_revision);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function eq(a, b)
|
|
852
|
+
{
|
|
853
|
+
if (a instanceof Object || b instanceof Object)
|
|
854
|
+
{
|
|
855
|
+
return stableStringify(a) === stableStringify(b);
|
|
856
|
+
}
|
|
857
|
+
return a == b;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function emptyObj(obj)
|
|
861
|
+
{
|
|
862
|
+
if (!obj)
|
|
863
|
+
{
|
|
864
|
+
return true;
|
|
865
|
+
}
|
|
866
|
+
for (const k in obj)
|
|
867
|
+
{
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
EtcTree.eq = eq;
|
|
874
|
+
|
|
875
|
+
module.exports = EtcTree;
|