autobee 2.0.0-rc.9 → 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/README.md +48 -1
- package/encoding/legacy.js +30 -5
- package/encoding/spec/autobee/index.js +289 -10
- package/encoding/spec/autobee/schema.json +217 -1
- package/index.js +548 -321
- package/lib/apply-calls.js +5 -6
- package/lib/apply-view.js +25 -0
- package/lib/boot.js +14 -182
- package/lib/constants.js +3 -6
- package/lib/encoding.js +72 -3
- package/lib/fast-forward.js +346 -0
- package/lib/migrations.js +457 -0
- package/lib/promotions.js +164 -0
- package/lib/system.js +97 -179
- package/lib/topo.js +83 -88
- package/lib/trusted.js +44 -0
- package/lib/updates.js +1 -38
- package/lib/witness.js +34 -65
- package/lib/writers.js +180 -116
- package/package.json +4 -3
- package/lib/reboot.js +0 -238
package/lib/system.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const Hyperbee = require('hyperbee2')
|
|
2
|
+
const Promotions = require('./promotions.js')
|
|
2
3
|
const b4a = require('b4a')
|
|
3
4
|
|
|
4
5
|
const { AUTOBEE_VERSION } = require('./constants')
|
|
@@ -43,31 +44,39 @@ module.exports = class Systembee {
|
|
|
43
44
|
this.version = 0
|
|
44
45
|
this.timestamp = 0
|
|
45
46
|
this.flushes = 0
|
|
46
|
-
this.indexers = null // legacy
|
|
47
47
|
this.updates = new Map()
|
|
48
48
|
this.writers = new Map()
|
|
49
|
-
this.shared = null
|
|
50
49
|
this.encrypted = opts.encrypted === true
|
|
50
|
+
this.promotions = new Promotions(this.bee)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
async addWriter(
|
|
54
|
+
key,
|
|
55
|
+
{ length = 0, weight = 1, isGenesis = false, coord = null, carrier = -1, anchor = false } = {}
|
|
56
|
+
) {
|
|
56
57
|
const info = await this.get(key, { unflushed: true })
|
|
57
58
|
if (length === 0) {
|
|
58
59
|
length = info ? info.length : 0
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
const effective = coord && carrier < weight ? Math.max(carrier, 0) : weight
|
|
63
|
+
const hint =
|
|
64
|
+
anchor && coord ? { key: coord.key, length: coord.length, weight: effective } : null
|
|
65
|
+
const pending = coord && !isGenesis ? weight : -1
|
|
66
|
+
|
|
61
67
|
const current = info ? info.maxWeight : 0
|
|
62
|
-
if (
|
|
63
|
-
|
|
68
|
+
if (effective <= current) {
|
|
69
|
+
// isAdded is false only when nothing has recorded the add yet - skipping
|
|
70
|
+
// the update then leaves the writer undiscoverable, never announced
|
|
71
|
+
const added = info && info.isAdded !== false
|
|
72
|
+
if (added && !info.isRemoved && length <= info.length && !hint && pending === -1) return
|
|
64
73
|
|
|
65
|
-
this.update(key, length, -1, -1, false, true, false,
|
|
74
|
+
this.update(key, length, -1, -1, false, true, false, -1, hint, pending)
|
|
66
75
|
return
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
const w = isGenesis ?
|
|
70
|
-
this.update(key, length, w,
|
|
78
|
+
const w = isGenesis ? effective : -1
|
|
79
|
+
this.update(key, length, w, effective, isGenesis, true, false, -1, hint, pending)
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
async ackWriter(key, { length = 0 } = {}) {
|
|
@@ -75,7 +84,7 @@ module.exports = class Systembee {
|
|
|
75
84
|
const info = await this.get(key, { unflushed: false })
|
|
76
85
|
length = info ? info.length : 0
|
|
77
86
|
}
|
|
78
|
-
this.update(key, length, -1, -1, false, false, false,
|
|
87
|
+
this.update(key, length, -1, -1, false, false, false, -1, null)
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
async removeWriter(key, { length = 0 } = {}) {
|
|
@@ -83,17 +92,24 @@ module.exports = class Systembee {
|
|
|
83
92
|
const info = await this.get(key, { unflushed: true })
|
|
84
93
|
length = info ? info.length : 0
|
|
85
94
|
}
|
|
86
|
-
this.update(key, length, -1, 0, false, false, true,
|
|
95
|
+
this.update(key, length, -1, 0, false, false, true, -1, null)
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
addAnchor(key
|
|
90
|
-
this.update(key,
|
|
98
|
+
addAnchor(key) {
|
|
99
|
+
this.update(key, 0, -1, -1, false, true, false, -1, null)
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
isGenesis() {
|
|
94
103
|
return this.bee.head() === null || this.bee.head().length === 0
|
|
95
104
|
}
|
|
96
105
|
|
|
106
|
+
isHead(key) {
|
|
107
|
+
for (const head of this.heads) {
|
|
108
|
+
if (b4a.equals(head.key, key)) return true
|
|
109
|
+
}
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
97
113
|
async boot(view, { timeout } = {}) {
|
|
98
114
|
await this.bee.ready()
|
|
99
115
|
this.bee.move(view)
|
|
@@ -115,60 +131,6 @@ module.exports = class Systembee {
|
|
|
115
131
|
}
|
|
116
132
|
}
|
|
117
133
|
|
|
118
|
-
async commonAncestor(head) {
|
|
119
|
-
const snap = this.bee.checkout(head)
|
|
120
|
-
|
|
121
|
-
const ca = infoIterator(this.bee)
|
|
122
|
-
const cb = infoIterator(snap)
|
|
123
|
-
|
|
124
|
-
let l = await next(ca)
|
|
125
|
-
let r = await next(cb)
|
|
126
|
-
|
|
127
|
-
while (l && r) {
|
|
128
|
-
if (l.legacy && r.legacy) {
|
|
129
|
-
// legacy nodes just compare head
|
|
130
|
-
if (sameHead(l.node.head, r.node.head)) {
|
|
131
|
-
return {
|
|
132
|
-
flushes: l.info.flushes,
|
|
133
|
-
system: l.node.head,
|
|
134
|
-
view: l.info.view || EMPTY_HEAD
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// different legacy cores never converge - genuinely unrelated history
|
|
139
|
-
if (!b4a.equals(l.node.head.key, r.node.head.key)) return null
|
|
140
|
-
|
|
141
|
-
if (l.node.head.length >= r.node.head.length) l = await next(ca)
|
|
142
|
-
else r = await next(cb)
|
|
143
|
-
continue
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (l.legacy !== r.legacy) {
|
|
147
|
-
// current-format history sits above the legacy band: descend it
|
|
148
|
-
if (!l.legacy) l = await next(ca)
|
|
149
|
-
else r = await next(cb)
|
|
150
|
-
continue
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (l.info.flushes === r.info.flushes && sameHead(l.info.view, r.info.view)) {
|
|
154
|
-
return {
|
|
155
|
-
flushes: l.info.flushes,
|
|
156
|
-
system: l.node.head,
|
|
157
|
-
view: l.info.view || EMPTY_HEAD
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const lf = l.info.flushes
|
|
162
|
-
const rf = r.info.flushes
|
|
163
|
-
|
|
164
|
-
if (lf >= rf) l = await next(ca)
|
|
165
|
-
if (rf >= lf) r = await next(cb)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// no common ancestor
|
|
169
|
-
return null
|
|
170
|
-
}
|
|
171
|
-
|
|
172
134
|
async reset({ timeout } = {}) {
|
|
173
135
|
const info = await this.getInfo({ timeout })
|
|
174
136
|
|
|
@@ -177,10 +139,9 @@ module.exports = class Systembee {
|
|
|
177
139
|
this.heads = info ? info.heads : []
|
|
178
140
|
this.timestamp = info ? info.timestamp : 0
|
|
179
141
|
this.flushes = info ? info.flushes : 0
|
|
180
|
-
this.indexers = info ? info.indexers : null
|
|
181
|
-
this.shared = null
|
|
182
142
|
this.updates.clear()
|
|
183
143
|
this.writers.clear()
|
|
144
|
+
this.promotions.reset(info)
|
|
184
145
|
|
|
185
146
|
if (this.version > AUTOBEE_VERSION) {
|
|
186
147
|
throw new Error('Autobee signals newer version than locally supported')
|
|
@@ -200,16 +161,8 @@ module.exports = class Systembee {
|
|
|
200
161
|
async undo(head) {
|
|
201
162
|
this.bee.move(head)
|
|
202
163
|
|
|
203
|
-
const shared = this.shared
|
|
204
164
|
await this.reset()
|
|
205
165
|
|
|
206
|
-
// deepest rewind since last mark - everything at or below it is untouched
|
|
207
|
-
if (shared === null || this.flushes < shared.flushes) {
|
|
208
|
-
this.shared = { flushes: this.flushes, view: this.view || EMPTY_HEAD, system: head }
|
|
209
|
-
} else if (shared) {
|
|
210
|
-
this.shared = shared
|
|
211
|
-
}
|
|
212
|
-
|
|
213
166
|
return this.view || EMPTY_HEAD
|
|
214
167
|
}
|
|
215
168
|
|
|
@@ -238,19 +191,7 @@ module.exports = class Systembee {
|
|
|
238
191
|
|
|
239
192
|
// resolved sort weight, re-stamped on every reapplication - this is what
|
|
240
193
|
// topo reads back out of the changes stream
|
|
241
|
-
this.update(
|
|
242
|
-
node.key,
|
|
243
|
-
node.length,
|
|
244
|
-
node.weight,
|
|
245
|
-
-1,
|
|
246
|
-
false,
|
|
247
|
-
false,
|
|
248
|
-
false,
|
|
249
|
-
false,
|
|
250
|
-
false,
|
|
251
|
-
node.timestamp,
|
|
252
|
-
false
|
|
253
|
-
)
|
|
194
|
+
this.update(node.key, node.length, node.weight, -1, false, false, false, node.timestamp, null)
|
|
254
195
|
}
|
|
255
196
|
|
|
256
197
|
async canApply(key, optimistic) {
|
|
@@ -269,8 +210,11 @@ module.exports = class Systembee {
|
|
|
269
210
|
return new SystemSnapshot(bee)
|
|
270
211
|
}
|
|
271
212
|
|
|
272
|
-
async get(key, { unflushed = false, timeout } = {}) {
|
|
273
|
-
const node = await this.bee.get(encoding.encodeSystemWriterKey(key), {
|
|
213
|
+
async get(key, { unflushed = false, timeout, activeRequests } = {}) {
|
|
214
|
+
const node = await this.bee.get(encoding.encodeSystemWriterKey(key), {
|
|
215
|
+
timeout,
|
|
216
|
+
activeRequests
|
|
217
|
+
})
|
|
274
218
|
const info = node !== null ? encoding.decodeSystemWriter(node.key, node.value) : null
|
|
275
219
|
if (!unflushed) return info
|
|
276
220
|
|
|
@@ -288,16 +232,12 @@ module.exports = class Systembee {
|
|
|
288
232
|
isGenesis: upd.maxWeight !== -1 && !!upd.isGenesis,
|
|
289
233
|
length: upd.length,
|
|
290
234
|
clock: 0,
|
|
291
|
-
isAdded: upd.isAdded
|
|
292
|
-
isAcked: upd.isAcked,
|
|
293
|
-
isAnchor: upd.isAnchor
|
|
235
|
+
isAdded: upd.isAdded
|
|
294
236
|
}
|
|
295
237
|
}
|
|
296
238
|
|
|
297
239
|
if (upd.isAdded) info.isAdded = true
|
|
298
240
|
if (upd.isRemoved) info.isRemoved = true
|
|
299
|
-
if (upd.isAcked) info.isAcked = true
|
|
300
|
-
if (upd.isAnchor) info.isAnchor = true
|
|
301
241
|
if (upd.maxWeight !== -1) {
|
|
302
242
|
info.isGenesis = upd.isGenesis
|
|
303
243
|
info.maxWeight = upd.maxWeight
|
|
@@ -338,17 +278,16 @@ module.exports = class Systembee {
|
|
|
338
278
|
isGenesis,
|
|
339
279
|
isAdded,
|
|
340
280
|
isRemoved,
|
|
341
|
-
isAcked,
|
|
342
|
-
isAnchor,
|
|
343
281
|
timestamp,
|
|
344
|
-
|
|
282
|
+
grant,
|
|
283
|
+
pending = -1
|
|
345
284
|
) {
|
|
346
285
|
const id = b4a.toString(key, 'hex')
|
|
347
286
|
|
|
348
287
|
if (isAdded) {
|
|
349
|
-
this.writers.set(id, { key, added: true
|
|
288
|
+
this.writers.set(id, { key, added: true })
|
|
350
289
|
} else if (isRemoved) {
|
|
351
|
-
this.writers.set(id, { key, added: false
|
|
290
|
+
this.writers.set(id, { key, added: false })
|
|
352
291
|
}
|
|
353
292
|
|
|
354
293
|
let upd = this.updates.get(id)
|
|
@@ -362,10 +301,9 @@ module.exports = class Systembee {
|
|
|
362
301
|
isGenesis: false,
|
|
363
302
|
isRemoved: false,
|
|
364
303
|
isAdded: false,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
needsWitness
|
|
304
|
+
timestamp: 0,
|
|
305
|
+
grant: null,
|
|
306
|
+
pending: -1
|
|
369
307
|
}
|
|
370
308
|
this.updates.set(id, upd)
|
|
371
309
|
}
|
|
@@ -376,7 +314,6 @@ module.exports = class Systembee {
|
|
|
376
314
|
upd.maxWeight = maxWeight
|
|
377
315
|
upd.isGenesis = isGenesis
|
|
378
316
|
}
|
|
379
|
-
if (isAnchor) upd.isAnchor = true
|
|
380
317
|
if (isAdded) {
|
|
381
318
|
upd.isAdded = true
|
|
382
319
|
upd.isRemoved = false
|
|
@@ -385,18 +322,16 @@ module.exports = class Systembee {
|
|
|
385
322
|
upd.isRemoved = true
|
|
386
323
|
upd.isAdded = false
|
|
387
324
|
}
|
|
388
|
-
if (isAcked) {
|
|
389
|
-
upd.isAcked = true
|
|
390
|
-
}
|
|
391
325
|
if (length > upd.length) {
|
|
392
326
|
upd.length = length
|
|
393
327
|
}
|
|
394
328
|
|
|
395
|
-
if (timestamp
|
|
329
|
+
if (timestamp > -1) {
|
|
396
330
|
upd.timestamp = timestamp
|
|
397
331
|
}
|
|
398
|
-
if (
|
|
399
|
-
|
|
332
|
+
if (pending > upd.pending) upd.pending = pending
|
|
333
|
+
if (grant && (upd.grant === null || grant.weight > upd.grant.weight)) {
|
|
334
|
+
upd.grant = grant
|
|
400
335
|
}
|
|
401
336
|
}
|
|
402
337
|
|
|
@@ -411,55 +346,54 @@ module.exports = class Systembee {
|
|
|
411
346
|
const isOplog = b4a.equals(upd.key, oplog)
|
|
412
347
|
|
|
413
348
|
const record = {
|
|
414
|
-
version:
|
|
349
|
+
version: 5,
|
|
415
350
|
key: upd.key,
|
|
416
351
|
isRemoved: upd.isRemoved ? true : upd.isAdded ? false : v ? v.isRemoved : false,
|
|
417
352
|
isOplog,
|
|
353
|
+
isGenesis: upd.maxWeight !== -1 ? !!upd.isGenesis : v ? v.isGenesis : false,
|
|
418
354
|
weight: upd.weight !== -1 ? upd.weight : prevWeight,
|
|
419
355
|
maxWeight: upd.maxWeight !== -1 ? upd.maxWeight : prevMaxWeight,
|
|
420
|
-
isGenesis: upd.maxWeight !== -1 ? !!upd.isGenesis : v ? v.isGenesis : false,
|
|
421
356
|
length: Math.max(upd.length, v ? v.length : 0),
|
|
422
|
-
|
|
423
|
-
isAnchor: upd.isAnchor || (v ? v.isAnchor : false),
|
|
424
|
-
timestamp: isOplog ? upd.timestamp : -1
|
|
357
|
+
timestamp: isOplog ? upd.timestamp : 0
|
|
425
358
|
}
|
|
426
359
|
|
|
427
360
|
return {
|
|
428
361
|
key: k,
|
|
429
362
|
value: encoding.encodeSystemWriter(record),
|
|
430
363
|
add: null,
|
|
431
|
-
remove:
|
|
364
|
+
remove: null,
|
|
432
365
|
maxWeight: record.maxWeight
|
|
433
366
|
}
|
|
434
367
|
}
|
|
435
368
|
|
|
436
369
|
async flush(batch, bee) {
|
|
437
|
-
if (batch.length === 0) return
|
|
370
|
+
if (batch.length === 0) return []
|
|
438
371
|
|
|
439
372
|
const oplog = batch[batch.length - 1].key
|
|
440
373
|
|
|
441
374
|
const updates = [...this.updates.values()]
|
|
442
|
-
const results = await Promise.all(updates.map((upd) => this.
|
|
443
|
-
|
|
444
|
-
const witnessed = []
|
|
445
|
-
for (let i = 0; i < updates.length; i++) {
|
|
446
|
-
if (updates[i].needsWitness) {
|
|
447
|
-
witnessed.push({ key: updates[i].key, weight: results[i].maxWeight })
|
|
448
|
-
}
|
|
449
|
-
}
|
|
375
|
+
const results = await Promise.all(updates.map((upd) => this._flushUpdate(upd, oplog)))
|
|
376
|
+
const pendings = await this.promotions.flushPending(updates)
|
|
450
377
|
|
|
451
378
|
const w = this.bee.write()
|
|
452
379
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
380
|
+
for (const { writer } of results) {
|
|
381
|
+
w.tryPut(writer.key, writer.value)
|
|
382
|
+
if (writer.add) w.tryPut(writer.add, EMPTY)
|
|
383
|
+
if (writer.remove) w.tryDelete(writer.remove)
|
|
457
384
|
}
|
|
458
385
|
|
|
459
|
-
for (const {
|
|
460
|
-
|
|
461
|
-
if (
|
|
462
|
-
|
|
386
|
+
for (const { grant } of results) {
|
|
387
|
+
if (!grant) continue
|
|
388
|
+
if (grant.remove) w.tryDelete(grant.key)
|
|
389
|
+
else w.tryPut(grant.key, grant.value)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (pendings) {
|
|
393
|
+
for (const p of pendings) {
|
|
394
|
+
if (p.remove) w.tryDelete(p.key)
|
|
395
|
+
else w.tryPut(p.key, p.value)
|
|
396
|
+
}
|
|
463
397
|
}
|
|
464
398
|
|
|
465
399
|
this.flushes++
|
|
@@ -470,11 +404,15 @@ module.exports = class Systembee {
|
|
|
470
404
|
heads: this.heads,
|
|
471
405
|
timestamp: this.timestamp,
|
|
472
406
|
flushes: this.flushes,
|
|
473
|
-
indexers: null // legacy
|
|
407
|
+
indexers: null, // legacy
|
|
408
|
+
pending: this.promotions.digest
|
|
474
409
|
}
|
|
475
410
|
|
|
476
411
|
w.tryPut(INFO_KEY, encoding.encodeSystemInfo(info))
|
|
477
412
|
|
|
413
|
+
this.version = info.version
|
|
414
|
+
this.view = info.view
|
|
415
|
+
|
|
478
416
|
await w.flush()
|
|
479
417
|
|
|
480
418
|
// Can enable if needed during debuggin
|
|
@@ -482,64 +420,43 @@ module.exports = class Systembee {
|
|
|
482
420
|
|
|
483
421
|
this.updates.clear()
|
|
484
422
|
|
|
485
|
-
if (this.writers.size === 0) return
|
|
423
|
+
if (this.writers.size === 0) return []
|
|
486
424
|
|
|
487
425
|
const changes = [...this.writers.values()]
|
|
488
426
|
|
|
489
427
|
this.writers.clear()
|
|
490
428
|
|
|
491
|
-
return
|
|
492
|
-
witnessed,
|
|
493
|
-
changed: changes
|
|
494
|
-
}
|
|
429
|
+
return changes
|
|
495
430
|
}
|
|
496
|
-
}
|
|
497
431
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
432
|
+
async _flushUpdate(upd, oplog) {
|
|
433
|
+
const [writer, grant] = await Promise.all([
|
|
434
|
+
this._updateWriter(upd, oplog),
|
|
435
|
+
this.promotions.updateHint(upd)
|
|
436
|
+
])
|
|
502
437
|
|
|
503
|
-
|
|
504
|
-
try {
|
|
505
|
-
return encoding.decodeSystemInfo(k.value)
|
|
506
|
-
} catch {
|
|
507
|
-
return null
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
438
|
+
return { writer, grant }
|
|
511
439
|
}
|
|
512
440
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
function sameHead(a, b) {
|
|
517
|
-
if (!a || !b || a.length !== b.length) return false
|
|
518
|
-
if (!a.key || !b.key) return a.key === b.key
|
|
519
|
-
return b4a.equals(a.key, b.key)
|
|
520
|
-
}
|
|
441
|
+
pendingPromotion(key, opts) {
|
|
442
|
+
return this.promotions.pending(key, opts)
|
|
443
|
+
}
|
|
521
444
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
for await (const node of bee.createChangesStream()) {
|
|
525
|
-
const legacy = topo.isLegacyNode(node)
|
|
526
|
-
const info = legacy ? getLegacyInfo(node) : getInfo(node)
|
|
527
|
-
if (info) yield { node, info, legacy }
|
|
528
|
-
}
|
|
529
|
-
} catch {
|
|
530
|
-
// fall through
|
|
445
|
+
listPendingPromotions(opts) {
|
|
446
|
+
return this.promotions.list(opts)
|
|
531
447
|
}
|
|
532
|
-
}
|
|
533
448
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
449
|
+
grantHint(key, opts) {
|
|
450
|
+
return this.promotions.hint(key, opts)
|
|
451
|
+
}
|
|
537
452
|
}
|
|
538
453
|
|
|
539
|
-
function
|
|
454
|
+
function getInfo(data) {
|
|
540
455
|
for (const { keys } of data.batch) {
|
|
541
456
|
for (const k of keys) {
|
|
542
|
-
|
|
457
|
+
const prefix = k.key[0]
|
|
458
|
+
|
|
459
|
+
if (prefix === 0) {
|
|
543
460
|
try {
|
|
544
461
|
return encoding.decodeSystemInfo(k.value)
|
|
545
462
|
} catch {
|
|
@@ -548,5 +465,6 @@ function getLegacyInfo(data) {
|
|
|
548
465
|
}
|
|
549
466
|
}
|
|
550
467
|
}
|
|
468
|
+
|
|
551
469
|
return null
|
|
552
470
|
}
|