corestore 6.0.0-beta2 → 6.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/test/all.js CHANGED
@@ -1,302 +1,334 @@
1
- const p = require('path')
1
+ const test = require('brittle')
2
+ const crypto = require('hypercore-crypto')
2
3
  const ram = require('random-access-memory')
3
- const raf = require('random-access-file')
4
- const test = require('tape')
5
- const hypercoreCrypto = require('hypercore-crypto')
6
- const { toPromises } = require('hypercore-promisifier')
7
- const OldCorestore = require('@andrewosh/corestore-migration')
4
+ const os = require('os')
5
+ const path = require('path')
6
+ const b4a = require('b4a')
7
+ const sodium = require('sodium-universal')
8
8
 
9
9
  const Corestore = require('..')
10
- const { cleanup } = require('./helpers')
11
10
 
12
- test('ram-based corestore, acceptable get options', async t => {
13
- const store = create(ram)
11
+ test('basic get with caching', async function (t) {
12
+ const store = new Corestore(ram)
13
+ const core1a = store.get({ name: 'core-1' })
14
+ const core1b = store.get({ name: 'core-1' })
15
+ const core2 = store.get({ name: 'core-2' })
14
16
 
15
- // A name option
16
- const core1 = store.get({ name: 'default' })
17
- await toPromises(core1).ready()
17
+ await Promise.all([core1a.ready(), core1b.ready(), core2.ready()])
18
18
 
19
- {
20
- // Buffer arg
21
- const core = store.get(core1.key)
22
- await toPromises(core).ready()
23
- t.same(core, core1)
24
- }
19
+ t.alike(core1a.key, core1b.key)
20
+ t.unlike(core1a.key, core2.key)
25
21
 
26
- {
27
- // String arg
28
- const core = store.get(core1.key.toString('hex'))
29
- await toPromises(core).ready()
30
- t.same(core, core1)
31
- }
22
+ t.ok(core1a.writable)
23
+ t.ok(core1b.writable)
32
24
 
33
- {
34
- // Object arg
35
- const core = store.get({ key: core1.key })
36
- await toPromises(core).ready()
37
- t.same(core, core1)
38
- }
39
-
40
- {
41
- // Object arg with string key
42
- const core = store.get({ key: core1.key.toString('hex') })
43
- await toPromises(core).ready()
44
- t.same(core, core1)
45
- }
25
+ t.is(store.cores.size, 2)
26
+ })
46
27
 
47
- {
48
- // Custom keypair
49
- const core = store.get({ keyPair: { secretKey: core1.secretKey, publicKey: core1.key } })
50
- await toPromises(core).ready()
51
- t.same(core, core1)
52
- }
28
+ test('basic get with custom keypair', async function (t) {
29
+ const store = new Corestore(ram)
30
+ const kp1 = crypto.keyPair()
31
+ const kp2 = crypto.keyPair()
53
32
 
54
- t.end()
55
- })
33
+ const core1 = store.get(kp1)
34
+ const core2 = store.get(kp2)
35
+ await Promise.all([core1.ready(), core2.ready()])
56
36
 
57
- test('ram-based corestore, unacceptable get options', async t => {
58
- const store = create(ram)
59
- const badGets = [
60
- () => store.get(),
61
- () => store.get('abc'),
62
- () => store.get({ name: null }),
63
- () => store.get({ key: null })
64
- ]
65
- for (const get of badGets) {
66
- try {
67
- get()
68
- t.fail('get did not throw correctly')
69
- } catch (err) {
70
- t.true(err)
71
- }
72
- }
37
+ t.alike(core1.key, kp1.publicKey)
38
+ t.alike(core2.key, kp2.publicKey)
39
+ t.ok(core1.writable)
40
+ t.ok(core2.writable)
73
41
  })
74
42
 
75
- test('ram-based corestore, many gets before ready', async t => {
76
- const store = create(ram)
77
- const core1 = store.get({ name: 'core1' })
78
- const core2 = store.get({ name: 'core1' })
79
- const core3 = store.get({ name: 'core3' })
80
-
81
- await Promise.all([
82
- toPromises(core1).ready(),
83
- toPromises(core2).ready(),
84
- toPromises(core3).ready()
85
- ])
43
+ test('basic namespaces', async function (t) {
44
+ const store = new Corestore(ram)
45
+ const ns1 = store.namespace('ns1')
46
+ const ns2 = store.namespace('ns2')
47
+ const ns3 = store.namespace('ns1') // Duplicate namespace
86
48
 
87
- t.same(core1, core2)
88
- t.notSame(core1, core3)
49
+ const core1 = ns1.get({ name: 'main' })
50
+ const core2 = ns2.get({ name: 'main' })
51
+ const core3 = ns3.get({ name: 'main' })
52
+ await Promise.all([core1.ready(), core2.ready(), core3.ready()])
89
53
 
90
- // At this point, the pre-ready cores should've been moved to the main cache.
91
- t.same(store.cache.size, 2)
54
+ t.absent(core1.key.equals(core2.key))
55
+ t.ok(core1.key.equals(core3.key))
56
+ t.ok(core1.writable)
57
+ t.ok(core2.writable)
58
+ t.ok(core3.writable)
59
+ t.is(store.cores.size, 2)
92
60
 
93
61
  t.end()
94
62
  })
95
63
 
96
- test('ram-based corestore, closing a ref decrements refcount', async t => {
97
- const store = create(ram)
98
- const core = toPromises(store.get({ name: 'core1' }))
99
- await core.ready()
64
+ test('basic replication', async function (t) {
65
+ const store1 = new Corestore(ram)
66
+ const store2 = new Corestore(ram)
100
67
 
101
- const cacheEntry = store.cache.entries.get(core.discoveryKey.toString('hex'))
102
- t.same(cacheEntry.refs, 1)
68
+ const core1 = store1.get({ name: 'core-1' })
69
+ const core2 = store1.get({ name: 'core-2' })
70
+ await core1.append('hello')
71
+ await core2.append('world')
103
72
 
104
- await core.close()
105
- t.same(cacheEntry.refs, 0)
73
+ const core3 = store2.get({ key: core1.key })
74
+ const core4 = store2.get({ key: core2.key })
106
75
 
107
- t.end()
76
+ const s = store1.replicate(true)
77
+ s.pipe(store2.replicate(false)).pipe(s)
78
+
79
+ t.alike(await core3.get(0), Buffer.from('hello'))
80
+ t.alike(await core4.get(0), Buffer.from('world'))
108
81
  })
109
82
 
110
- test('ram-based corestore, simple replication', async t => {
111
- const store1 = create(ram)
112
- const store2 = create(ram)
83
+ test('replicating cores created after replication begins', async function (t) {
84
+ const store1 = new Corestore(ram)
85
+ const store2 = new Corestore(ram)
113
86
 
114
- const core1 = toPromises(store1.get({ name: 'core1', valueEncoding: 'utf-8' }))
87
+ const s = store1.replicate(true, { live: true })
88
+ s.pipe(store2.replicate(false, { live: true })).pipe(s)
89
+
90
+ const core1 = store1.get({ name: 'core-1' })
91
+ const core2 = store1.get({ name: 'core-2' })
115
92
  await core1.append('hello')
93
+ await core2.append('world')
116
94
 
117
- const s1 = store1.replicate(true, { live: true })
118
- s1.pipe(store2.replicate(false, { live: true })).pipe(s1)
95
+ const core3 = store2.get({ key: core1.key })
96
+ const core4 = store2.get({ key: core2.key })
119
97
 
120
- const clone1 = toPromises(store2.get({ key: core1.key, valueEncoding: 'utf-8' }))
121
- t.same(await clone1.get(0), 'hello')
98
+ t.alike(await core3.get(0), Buffer.from('hello'))
99
+ t.alike(await core4.get(0), Buffer.from('world'))
100
+ })
122
101
 
123
- const core2 = toPromises(store1.get({ name: 'core2', valueEncoding: 'utf-8' }))
124
- await core2.append('world')
102
+ test('replicating cores using discovery key hook', async function (t) {
103
+ const dir = tmpdir()
104
+ let store1 = new Corestore(dir)
105
+ const store2 = new Corestore(ram)
125
106
 
126
- const clone2 = toPromises(store2.get({ key: core2.key, valueEncoding: 'utf-8' }))
127
- t.same(await clone2.get(0), 'world')
107
+ const core = store1.get({ name: 'main' })
108
+ await core.append('hello')
109
+ const key = core.key
128
110
 
129
- t.end()
111
+ await store1.close()
112
+ store1 = new Corestore(dir)
113
+
114
+ const s = store1.replicate(true, { live: true })
115
+ s.pipe(store2.replicate(false, { live: true })).pipe(s)
116
+
117
+ const core2 = store2.get(key)
118
+ t.alike(await core2.get(0), Buffer.from('hello'))
130
119
  })
131
120
 
132
- test('ram-based corestore, sparse replication', async t => {
133
- const store1 = create(ram, { sparse: true })
134
- const store2 = create(ram, { sparse: true })
121
+ test('nested namespaces', async function (t) {
122
+ const store = new Corestore(ram)
123
+ const ns1a = store.namespace('ns1').namespace('a')
124
+ const ns1b = store.namespace('ns1').namespace('b')
135
125
 
136
- const core1 = toPromises(store1.get({ name: 'core1', valueEncoding: 'utf-8' }))
137
- await core1.append('hello')
126
+ const core1 = ns1a.get({ name: 'main' })
127
+ const core2 = ns1b.get({ name: 'main' })
128
+ await Promise.all([core1.ready(), core2.ready()])
138
129
 
139
- const s1 = store1.replicate(true, { live: true })
140
- s1.pipe(store2.replicate(false, { live: true })).pipe(s1)
130
+ t.not(core1.key.equals(core2.key))
131
+ t.ok(core1.writable)
132
+ t.ok(core2.writable)
133
+ t.is(store.cores.size, 2)
134
+ })
141
135
 
142
- const clone1 = toPromises(store2.get({ key: core1.key, valueEncoding: 'utf-8' }))
143
- t.same(await clone1.get(0), 'hello')
136
+ test('core uncached when all sessions close', async function (t) {
137
+ const store = new Corestore(ram)
138
+ const core1 = store.get({ name: 'main' })
139
+ await core1.ready()
140
+ t.is(store.cores.size, 1)
141
+ await core1.close()
142
+ t.is(store.cores.size, 0)
143
+ })
144
144
 
145
- const core2 = toPromises(store1.get({ name: 'core2', valueEncoding: 'utf-8' }))
146
- await core2.append('world')
145
+ test('writable core loaded from name userData', async function (t) {
146
+ const dir = tmpdir()
147
147
 
148
- const clone2 = toPromises(store2.get({ key: core2.key, valueEncoding: 'utf-8' }))
149
- t.same(await clone2.get(0), 'world')
148
+ let store = new Corestore(dir)
149
+ let core = store.get({ name: 'main' })
150
+ await core.ready()
151
+ const key = core.key
150
152
 
151
- t.end()
153
+ t.ok(core.writable)
154
+ await core.append('hello')
155
+ t.is(core.length, 1)
156
+
157
+ await store.close()
158
+ store = new Corestore(dir)
159
+ core = store.get(key)
160
+ await core.ready()
161
+
162
+ t.ok(core.writable)
163
+ await core.append('world')
164
+ t.is(core.length, 2)
165
+ t.alike(await core.get(0), Buffer.from('hello'))
166
+ t.alike(await core.get(1), Buffer.from('world'))
152
167
  })
153
168
 
154
- test('raf-based corestore, simple replication', async t => {
155
- const store1 = create(path => raf(p.join('store1', path)))
156
- const store2 = create(path => raf(p.join('store2', path)))
169
+ test('writable core loaded from name and namespace userData', async function (t) {
170
+ const dir = tmpdir()
157
171
 
158
- const core1 = toPromises(store1.get({ name: 'core1', valueEncoding: 'utf-8' }))
159
- await core1.append('hello')
172
+ let store = new Corestore(dir)
173
+ let core = store.namespace('ns1').get({ name: 'main' })
174
+ await core.ready()
175
+ const key = core.key
160
176
 
161
- const s1 = store1.replicate(true, { live: true })
162
- s1.pipe(store2.replicate(false, { live: true })).pipe(s1)
177
+ t.ok(core.writable)
178
+ await core.append('hello')
179
+ t.is(core.length, 1)
163
180
 
164
- const clone1 = toPromises(store2.get({ key: core1.key, valueEncoding: 'utf-8' }))
165
- t.same(await clone1.get(0), 'hello')
181
+ await store.close()
182
+ store = new Corestore(dir)
183
+ core = store.get(key)
184
+ await core.ready()
166
185
 
167
- const core2 = toPromises(store1.get({ name: 'core2', valueEncoding: 'utf-8' }))
168
- await core2.append('world')
186
+ t.ok(core.writable)
187
+ await core.append('world')
188
+ t.is(core.length, 2)
189
+ t.alike(await core.get(0), Buffer.from('hello'))
190
+ t.alike(await core.get(1), Buffer.from('world'))
191
+ })
169
192
 
170
- const clone2 = toPromises(store2.get({ key: core2.key, valueEncoding: 'utf-8' }))
171
- t.same(await clone2.get(0), 'world')
193
+ test('storage locking', async function (t) {
194
+ const dir = tmpdir()
172
195
 
173
- await cleanup(['store1', 'store2'])
174
- t.end()
196
+ const store1 = new Corestore(dir)
197
+ await store1.ready()
198
+
199
+ const store2 = new Corestore(dir)
200
+ try {
201
+ await store2.ready()
202
+ t.fail('dir should have been locked')
203
+ } catch {
204
+ t.pass('dir was locked')
205
+ }
175
206
  })
176
207
 
177
- test('raf-based corestore, close and reopen', async t => {
178
- let store = create('test-store')
208
+ test('closing a namespace does not close cores', async function (t) {
209
+ const store = new Corestore(ram)
210
+ const ns1 = store.namespace('ns1')
211
+ const core1 = ns1.get({ name: 'core-1' })
212
+ const core2 = ns1.get({ name: 'core-2' })
213
+ await Promise.all([core1.ready(), core2.ready()])
179
214
 
180
- let core1 = toPromises(store.get({ name: 'core1', valueEncoding: 'utf-8' }))
181
- await core1.append('hello')
215
+ await ns1.close()
182
216
 
183
- t.same(await core1.get(0), 'hello')
217
+ t.is(store.cores.size, 2)
218
+ t.not(core1.closed)
219
+ t.not(core1.closed)
184
220
 
185
221
  await store.close()
186
- store = create('test-store')
187
- core1 = toPromises(store.get({ name: 'core1', valueEncoding: 'utf-8' }))
188
-
189
- t.same(await core1.get(0), 'hello')
190
222
 
191
- await cleanup(['test-store'])
192
- t.end()
223
+ t.is(store.cores.size, 0)
224
+ t.ok(core1.closed)
225
+ t.ok(core2.closed)
193
226
  })
194
227
 
195
- test('raf-based corestore, close and reopen with keypair option', async t => {
196
- let store = create('test-store')
197
- const keyPair = hypercoreCrypto.keyPair()
228
+ test('findingPeers', async function (t) {
229
+ t.plan(6)
198
230
 
199
- let core1 = toPromises(store.get({ keyPair, valueEncoding: 'utf-8' }))
200
- await core1.append('hello')
231
+ const store = new Corestore(ram)
201
232
 
202
- t.same(await core1.get(0), 'hello')
233
+ const ns1 = store.namespace('ns1')
234
+ const ns2 = store.namespace('ns2')
203
235
 
204
- await store.close()
205
- store = create('test-store')
206
- core1 = toPromises(store.get({ keyPair, valueEncoding: 'utf-8' }))
236
+ const a = ns1.get(Buffer.alloc(32).fill('a'))
237
+ const b = ns2.get(Buffer.alloc(32).fill('b'))
207
238
 
208
- t.same(await core1.get(0), 'hello')
239
+ const done = ns1.findingPeers()
209
240
 
210
- await cleanup(['test-store'])
211
- t.end()
212
- })
241
+ let aUpdated = false
242
+ let bUpdated = false
243
+ let cUpdated = false
213
244
 
214
- test('namespace method is equivalent to name array', async t => {
215
- const store = create(ram)
245
+ const c = ns1.get(Buffer.alloc(32).fill('c'))
216
246
 
217
- const core1 = store.get({ name: ['a', 'b', 'c'] })
218
- const core2 = store.namespace('a').namespace('b').get({ name: 'c' })
247
+ a.update().then(function (bool) {
248
+ aUpdated = true
249
+ })
219
250
 
220
- await toPromises(core1).ready()
221
- await toPromises(core2).ready()
251
+ b.update().then(function (bool) {
252
+ bUpdated = true
253
+ })
222
254
 
223
- t.same(core1, core2)
224
- t.end()
255
+ c.update().then(function (bool) {
256
+ cUpdated = true
257
+ })
258
+
259
+ await new Promise(resolve => setImmediate(resolve))
260
+
261
+ t.is(aUpdated, false)
262
+ t.is(bUpdated, true)
263
+ t.is(cUpdated, false)
264
+
265
+ done()
266
+
267
+ await new Promise(resolve => setImmediate(resolve))
268
+
269
+ t.is(aUpdated, true)
270
+ t.is(bUpdated, true)
271
+ t.is(cUpdated, true)
225
272
  })
226
273
 
227
- test('can backup/restore', async t => {
228
- const firstStore = create(ram)
229
- const core1 = firstStore.get({ name: 'hello-world' })
230
- await toPromises(core1).ready()
231
- const manifest = await firstStore.backup()
274
+ test('different primary keys yield different keypairs', async function (t) {
275
+ const pk1 = randomBytes(32)
276
+ const pk2 = randomBytes(32)
277
+ t.unlike(pk1, pk2)
232
278
 
233
- const secondStore = await Corestore.restore(manifest, ram)
234
- const core2 = secondStore.get({ key: core1.key })
235
- await toPromises(core2).ready()
279
+ const store1 = new Corestore(ram, { primaryKey: pk1 })
280
+ const store2 = new Corestore(ram, { primaryKey: pk2 })
236
281
 
237
- t.true(core2.writable)
282
+ const kp1 = await store1.createKeyPair('hello')
283
+ const kp2 = await store2.createKeyPair('hello')
238
284
 
239
- t.end()
285
+ t.unlike(kp1.publicKey, kp2.publicKey)
240
286
  })
241
287
 
242
- test('can migrate an old corestore', async t => {
243
- const oldStore = new OldCorestore('test-store')
244
- await oldStore.ready()
245
-
246
- const oldCores = [
247
- oldStore.default(),
248
- oldStore.namespace('hello').default(),
249
- oldStore.namespace('hello').namespace('world').default(),
250
- oldStore.get({ name: 'test-name' }),
251
- // Randomly-generated names should also be migrated.
252
- oldStore.get()
253
- ]
254
- await Promise.all(oldCores.map(c => new Promise(resolve => c.ready(resolve))))
255
-
256
- const names = [
257
- 'default',
258
- ['hello'],
259
- ['hello', 'world'],
260
- 'test-name'
261
- ]
262
- const keys = oldCores.map(c => c.key)
263
- await new Promise(resolve => oldStore.close(resolve))
264
-
265
- {
266
- const store = create('test-store', {
267
- migrationRoot: 'test-store'
268
- })
269
-
270
- // Cores are writable when loaded by name
271
- for (let i = 0; i < names.length; i++) {
272
- const core = store.get({ name: names[i] })
273
- await toPromises(core).ready()
274
- t.true(core.writable)
275
- t.same(core.key, keys[i])
276
- }
277
-
278
- await store.close()
279
- }
288
+ test('keypair auth sign', async function (t) {
289
+ const store = new Corestore(ram)
290
+ const keyPair = await store.createKeyPair('foo')
291
+ const message = b4a.from('hello world')
280
292
 
281
- {
282
- // The second time should work without a migration.
283
- const store = create('test-store')
293
+ const sig = keyPair.auth.sign(message)
284
294
 
285
- // Cores are writable when loaded by key
286
- for (let i = 0; i < keys.length; i++) {
287
- const core = store.get({ key: keys[i] })
288
- await toPromises(core).ready()
289
- t.true(core.writable)
290
- }
295
+ t.is(sig.length, 64)
296
+ t.ok(crypto.verify(message, sig, keyPair.publicKey))
297
+ t.absent(crypto.verify(message, b4a.alloc(64), keyPair.publicKey))
298
+ })
291
299
 
292
- await store.close()
293
- }
300
+ test('keypair auth verify', async function (t) {
301
+ const store = new Corestore(ram)
302
+ const keyPair = await store.createKeyPair('foo')
303
+ const message = b4a.from('hello world')
294
304
 
295
- await cleanup(['test-store'])
296
- t.end()
305
+ const sig = crypto.sign(message, keyPair.secretKey)
306
+
307
+ t.is(sig.length, 64)
308
+ t.ok(keyPair.auth.verify(message, sig))
309
+ t.absent(keyPair.auth.verify(message, b4a.alloc(64)))
297
310
  })
298
311
 
299
- function create (storage, opts) {
300
- const store = new Corestore(storage, opts)
301
- return store
312
+ test('core caching after reopen regression', async function (t) {
313
+ const store = new Corestore(ram)
314
+ const core = store.get({ name: 'test-core' })
315
+ await core.ready()
316
+
317
+ core.close()
318
+ await core.opening
319
+
320
+ const core2 = store.get({ name: 'test-core' })
321
+ await core2.ready()
322
+
323
+ t.pass('did not infinite loop')
324
+ })
325
+
326
+ function tmpdir () {
327
+ return path.join(os.tmpdir(), 'corestore-' + Math.random().toString(16).slice(2))
328
+ }
329
+
330
+ function randomBytes (n) {
331
+ const buf = b4a.allocUnsafe(n)
332
+ sodium.randombytes_buf(buf)
333
+ return buf
302
334
  }
package/test/cache.js ADDED
@@ -0,0 +1,46 @@
1
+ const test = require('brittle')
2
+ const RAM = require('random-access-memory')
3
+
4
+ const Corestore = require('..')
5
+
6
+ test('core cache', async function (t) {
7
+ const store = new Corestore(RAM, { cache: true })
8
+
9
+ const core = store.get({ name: 'core' })
10
+ await core.append(['a', 'b', 'c'])
11
+
12
+ const p = core.get(0)
13
+ const q = core.get(0)
14
+
15
+ t.is(await p, await q)
16
+ })
17
+
18
+ test('clear cache on truncate', async function (t) {
19
+ const store = new Corestore(RAM, { cache: true })
20
+
21
+ const core = store.get({ name: 'core' })
22
+ await core.append(['a', 'b', 'c'])
23
+
24
+ const p = core.get(0)
25
+
26
+ await core.truncate(0)
27
+ await core.append('d')
28
+
29
+ const q = core.get(0)
30
+
31
+ t.alike(await p, Buffer.from('a'))
32
+ t.alike(await q, Buffer.from('d'))
33
+ })
34
+
35
+ test('core cache on namespace', async function (t) {
36
+ const store = new Corestore(RAM, { cache: true })
37
+ const ns1 = store.namespace('test-namespace-1')
38
+
39
+ const c1 = store.get({ name: 'test-core' })
40
+ const c2 = ns1.get({ name: 'test-core' })
41
+
42
+ await Promise.all([c1.ready(), c2.ready()])
43
+
44
+ t.ok(c1.cache)
45
+ t.ok(c2.cache)
46
+ })
@@ -1,12 +1,7 @@
1
- const fs = require('fs')
1
+ const fs = require('fs').promises
2
2
 
3
3
  async function cleanup (dirs) {
4
- return Promise.all(dirs.map(dir => new Promise((resolve, reject) => {
5
- fs.rmdir(dir, { recursive: true }, err => {
6
- if (err) return reject(err)
7
- return resolve()
8
- })
9
- })))
4
+ return Promise.allSettled(dirs.map(dir => fs.rmdir(dir, { recursive: true })))
10
5
  }
11
6
 
12
7
  function delay (ms, cb) {
@@ -1,26 +0,0 @@
1
- const RAS = require('random-access-storage')
2
-
3
- module.exports = class BufferFile extends RAS {
4
- constructor (buf) {
5
- super()
6
- this._buf = buf
7
- }
8
-
9
- _open (req) {
10
- if (typeof this._buf !== 'function') return req.callback(null)
11
- this._buf((err, b) => {
12
- if (err) return req.callback(err)
13
- this._buf = b
14
- req.callback(null)
15
- })
16
- }
17
-
18
- _stat (req) {
19
- req.callback(null, { size: this._buf ? this._buf.length : 0 })
20
- }
21
-
22
- _read (req) {
23
- if (!this._buf) return req.callback(new Error('Could not satisfy length'))
24
- req.callback(null, this._buf.slice(req.offset, req.offset + req.size))
25
- }
26
- }