effect-distributed-lock 0.0.6 → 0.0.8
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/examples/push.ts +109 -0
- package/package.json +1 -1
- package/src/DistributedSemaphore.ts +7 -8
- package/src/RedisBacking.ts +4 -4
- package/redis-semaphore/.codeclimate.yml +0 -5
- package/redis-semaphore/.fossa.yml +0 -14
- package/redis-semaphore/.github/dependabot.yml +0 -6
- package/redis-semaphore/.github/workflows/branches.yml +0 -39
- package/redis-semaphore/.github/workflows/pull-requests.yml +0 -35
- package/redis-semaphore/.mocharc.yaml +0 -6
- package/redis-semaphore/.prettierrc +0 -6
- package/redis-semaphore/.snyk +0 -4
- package/redis-semaphore/.yarnrc.yml +0 -2
- package/redis-semaphore/CHANGELOG.md +0 -70
- package/redis-semaphore/Dockerfile +0 -5
- package/redis-semaphore/LICENSE +0 -21
- package/redis-semaphore/README.md +0 -445
- package/redis-semaphore/docker-compose.yml +0 -31
- package/redis-semaphore/eslint.config.mjs +0 -73
- package/redis-semaphore/package.json +0 -79
- package/redis-semaphore/setup-redis-servers.sh +0 -2
- package/redis-semaphore/src/Lock.ts +0 -172
- package/redis-semaphore/src/RedisMultiSemaphore.ts +0 -56
- package/redis-semaphore/src/RedisMutex.ts +0 -45
- package/redis-semaphore/src/RedisSemaphore.ts +0 -49
- package/redis-semaphore/src/RedlockMultiSemaphore.ts +0 -56
- package/redis-semaphore/src/RedlockMutex.ts +0 -52
- package/redis-semaphore/src/RedlockSemaphore.ts +0 -49
- package/redis-semaphore/src/errors/LostLockError.ts +0 -1
- package/redis-semaphore/src/errors/TimeoutError.ts +0 -1
- package/redis-semaphore/src/index.ts +0 -23
- package/redis-semaphore/src/misc.ts +0 -12
- package/redis-semaphore/src/multiSemaphore/acquire/index.ts +0 -53
- package/redis-semaphore/src/multiSemaphore/acquire/lua.ts +0 -31
- package/redis-semaphore/src/multiSemaphore/refresh/index.ts +0 -32
- package/redis-semaphore/src/multiSemaphore/refresh/lua.ts +0 -31
- package/redis-semaphore/src/multiSemaphore/release/index.ts +0 -22
- package/redis-semaphore/src/multiSemaphore/release/lua.ts +0 -17
- package/redis-semaphore/src/mutex/acquire.ts +0 -42
- package/redis-semaphore/src/mutex/refresh.ts +0 -37
- package/redis-semaphore/src/mutex/release.ts +0 -30
- package/redis-semaphore/src/redlockMultiSemaphore/acquire.ts +0 -56
- package/redis-semaphore/src/redlockMultiSemaphore/refresh.ts +0 -68
- package/redis-semaphore/src/redlockMultiSemaphore/release.ts +0 -19
- package/redis-semaphore/src/redlockMutex/acquire.ts +0 -54
- package/redis-semaphore/src/redlockMutex/refresh.ts +0 -53
- package/redis-semaphore/src/redlockMutex/release.ts +0 -19
- package/redis-semaphore/src/redlockSemaphore/acquire.ts +0 -55
- package/redis-semaphore/src/redlockSemaphore/refresh.ts +0 -60
- package/redis-semaphore/src/redlockSemaphore/release.ts +0 -18
- package/redis-semaphore/src/semaphore/acquire/index.ts +0 -52
- package/redis-semaphore/src/semaphore/acquire/lua.ts +0 -25
- package/redis-semaphore/src/semaphore/refresh/index.ts +0 -31
- package/redis-semaphore/src/semaphore/refresh/lua.ts +0 -25
- package/redis-semaphore/src/semaphore/release.ts +0 -14
- package/redis-semaphore/src/types.ts +0 -63
- package/redis-semaphore/src/utils/createEval.ts +0 -45
- package/redis-semaphore/src/utils/index.ts +0 -13
- package/redis-semaphore/src/utils/redlock.ts +0 -7
- package/redis-semaphore/test/init.test.ts +0 -9
- package/redis-semaphore/test/redisClient.ts +0 -82
- package/redis-semaphore/test/setup.ts +0 -6
- package/redis-semaphore/test/shell.test.ts +0 -15
- package/redis-semaphore/test/shell.ts +0 -48
- package/redis-semaphore/test/src/Lock.test.ts +0 -37
- package/redis-semaphore/test/src/RedisMultiSemaphore.test.ts +0 -425
- package/redis-semaphore/test/src/RedisMutex.test.ts +0 -334
- package/redis-semaphore/test/src/RedisSemaphore.test.ts +0 -367
- package/redis-semaphore/test/src/RedlockMultiSemaphore.test.ts +0 -671
- package/redis-semaphore/test/src/RedlockMutex.test.ts +0 -328
- package/redis-semaphore/test/src/RedlockSemaphore.test.ts +0 -579
- package/redis-semaphore/test/src/index.test.ts +0 -22
- package/redis-semaphore/test/src/multiSemaphore/acquire/index.test.ts +0 -51
- package/redis-semaphore/test/src/multiSemaphore/acquire/internal.test.ts +0 -67
- package/redis-semaphore/test/src/multiSemaphore/refresh/index.test.ts +0 -52
- package/redis-semaphore/test/src/multiSemaphore/release/index.test.ts +0 -18
- package/redis-semaphore/test/src/mutex/acquire.test.ts +0 -78
- package/redis-semaphore/test/src/mutex/refresh.test.ts +0 -22
- package/redis-semaphore/test/src/mutex/release.test.ts +0 -17
- package/redis-semaphore/test/src/redlockMutex/acquire.test.ts +0 -90
- package/redis-semaphore/test/src/redlockMutex/refresh.test.ts +0 -27
- package/redis-semaphore/test/src/redlockMutex/release.test.ts +0 -17
- package/redis-semaphore/test/src/semaphore/acquire/index.test.ts +0 -49
- package/redis-semaphore/test/src/semaphore/acquire/internal.test.ts +0 -65
- package/redis-semaphore/test/src/semaphore/refresh/index.test.ts +0 -44
- package/redis-semaphore/test/src/semaphore/release.test.ts +0 -18
- package/redis-semaphore/test/src/utils/eval.test.ts +0 -22
- package/redis-semaphore/test/src/utils/index.test.ts +0 -19
- package/redis-semaphore/test/src/utils/redlock.test.ts +0 -31
- package/redis-semaphore/test/unhandledRejection.ts +0 -28
- package/redis-semaphore/tsconfig.build-commonjs.json +0 -9
- package/redis-semaphore/tsconfig.build-es.json +0 -9
- package/redis-semaphore/tsconfig.json +0 -11
- package/redis-semaphore/yarn.lock +0 -5338
- /package/examples/{index.ts → kitchen-sink.ts} +0 -0
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { createEval } from '../../utils/index'
|
|
2
|
-
|
|
3
|
-
export const refreshLua = createEval<
|
|
4
|
-
[string, number, string, number, number],
|
|
5
|
-
0 | 1
|
|
6
|
-
>(
|
|
7
|
-
`
|
|
8
|
-
local key = KEYS[1]
|
|
9
|
-
local limit = tonumber(ARGV[1])
|
|
10
|
-
local identifier = ARGV[2]
|
|
11
|
-
local lockTimeout = tonumber(ARGV[3])
|
|
12
|
-
local now = tonumber(ARGV[4])
|
|
13
|
-
local expiredTimestamp = now - lockTimeout
|
|
14
|
-
|
|
15
|
-
redis.call('zremrangebyscore', key, '-inf', expiredTimestamp)
|
|
16
|
-
|
|
17
|
-
if redis.call('zscore', key, identifier) then
|
|
18
|
-
redis.call('zadd', key, now, identifier)
|
|
19
|
-
redis.call('pexpire', key, lockTimeout)
|
|
20
|
-
return 1
|
|
21
|
-
else
|
|
22
|
-
return 0
|
|
23
|
-
end`,
|
|
24
|
-
1
|
|
25
|
-
)
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import createDebug from 'debug'
|
|
2
|
-
import { RedisClient } from '../types'
|
|
3
|
-
|
|
4
|
-
const debug = createDebug('redis-semaphore:semaphore:release')
|
|
5
|
-
|
|
6
|
-
export async function releaseSemaphore(
|
|
7
|
-
client: RedisClient,
|
|
8
|
-
key: string,
|
|
9
|
-
identifier: string
|
|
10
|
-
): Promise<void> {
|
|
11
|
-
debug(key, identifier)
|
|
12
|
-
const result = await client.zrem(key, identifier)
|
|
13
|
-
debug('result', typeof result, result)
|
|
14
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import LostLockError from './errors/LostLockError'
|
|
2
|
-
import { Lock } from './Lock'
|
|
3
|
-
|
|
4
|
-
import type * as ioredis from 'ioredis'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* ioredis-like Redis client
|
|
8
|
-
*/
|
|
9
|
-
export type RedisClient = Pick<
|
|
10
|
-
ioredis.Redis,
|
|
11
|
-
'eval' | 'evalsha' | 'get' | 'set' | 'zrem'
|
|
12
|
-
> &
|
|
13
|
-
Partial<Pick<ioredis.Redis, 'options'>>
|
|
14
|
-
|
|
15
|
-
export interface LockLostCallback {
|
|
16
|
-
(this: Lock, err: LostLockError): void
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface TimeoutOptions {
|
|
20
|
-
lockTimeout?: number
|
|
21
|
-
acquireTimeout?: number
|
|
22
|
-
acquireAttemptsLimit?: number
|
|
23
|
-
retryInterval?: number
|
|
24
|
-
refreshInterval?: number
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface LockOptions extends TimeoutOptions {
|
|
28
|
-
/**
|
|
29
|
-
* @deprecated Use `identifier` + `acquiredExternally: true` instead. Will be removed in next major release.
|
|
30
|
-
*/
|
|
31
|
-
externallyAcquiredIdentifier?: string
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @deprecated Provide custom `identifier` instead. Will be removed in next major release.
|
|
35
|
-
*/
|
|
36
|
-
identifierSuffix?: string
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Identifier of lock. By default is `crypto.randomUUID()`.
|
|
40
|
-
*
|
|
41
|
-
* Must be unique between parallel executors otherwise locks with same identifier *can* be treated as the same lock holder.
|
|
42
|
-
*
|
|
43
|
-
* Override only if you know what you are doing, see `acquireExternally` option.
|
|
44
|
-
*/
|
|
45
|
-
identifier?: string
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* If `identifier` provided and `acquiredExternally` is `true` then `_refresh` will be used instead of `_acquire` in `.tryAcquire()`/`.acquire()`.
|
|
49
|
-
*
|
|
50
|
-
* Useful for lock sharing between processes: acquire in scheduler, refresh and release in handler.
|
|
51
|
-
*/
|
|
52
|
-
acquiredExternally?: true
|
|
53
|
-
|
|
54
|
-
onLockLost?: LockLostCallback
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface AcquireOptions {
|
|
58
|
-
identifier: string
|
|
59
|
-
lockTimeout: number
|
|
60
|
-
acquireTimeout: number
|
|
61
|
-
acquireAttemptsLimit: number
|
|
62
|
-
retryInterval: number
|
|
63
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'crypto'
|
|
2
|
-
import createDebug from 'debug'
|
|
3
|
-
import { RedisClient } from '../types'
|
|
4
|
-
import { getConnectionName } from './index'
|
|
5
|
-
|
|
6
|
-
const debug = createDebug('redis-semaphore:eval')
|
|
7
|
-
|
|
8
|
-
function createSHA1(script: string): string {
|
|
9
|
-
return createHash('sha1').update(script, 'utf8').digest('hex')
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function isNoScriptError(err: Error): boolean {
|
|
13
|
-
return err.toString().indexOf('NOSCRIPT') !== -1
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export default function createEval<Args extends Array<number | string>, Result>(
|
|
17
|
-
script: string,
|
|
18
|
-
keysCount: number
|
|
19
|
-
): (client: RedisClient, args: Args) => Promise<Result> {
|
|
20
|
-
const sha1 = createSHA1(script)
|
|
21
|
-
debug('creating script:', script, 'sha1:', sha1)
|
|
22
|
-
return async function optimizedEval(
|
|
23
|
-
client: RedisClient,
|
|
24
|
-
args: Args
|
|
25
|
-
): Promise<Result> {
|
|
26
|
-
const connectionName = getConnectionName(client)
|
|
27
|
-
const evalSHAArgs = [sha1, keysCount, ...args]
|
|
28
|
-
debug(connectionName, sha1, 'attempt, args:', evalSHAArgs)
|
|
29
|
-
try {
|
|
30
|
-
return (await client.evalsha(sha1, keysCount, ...args)) as Promise<Result>
|
|
31
|
-
} catch (err) {
|
|
32
|
-
if (err instanceof Error && isNoScriptError(err)) {
|
|
33
|
-
const evalArgs = [script, keysCount, ...args]
|
|
34
|
-
debug(connectionName, sha1, 'fallback to eval, args:', evalArgs)
|
|
35
|
-
return (await client.eval(
|
|
36
|
-
script,
|
|
37
|
-
keysCount,
|
|
38
|
-
...args
|
|
39
|
-
)) as Promise<Result>
|
|
40
|
-
} else {
|
|
41
|
-
throw err
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { RedisClient } from '../types'
|
|
2
|
-
import createEval from './createEval'
|
|
3
|
-
|
|
4
|
-
export { createEval }
|
|
5
|
-
|
|
6
|
-
export async function delay(ms: number): Promise<void> {
|
|
7
|
-
return await new Promise(resolve => setTimeout(resolve, ms))
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function getConnectionName(client: RedisClient): string {
|
|
11
|
-
const connectionName = client.options?.connectionName
|
|
12
|
-
return connectionName ? `<${connectionName}>` : '<unknown client>'
|
|
13
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import Redis from 'ioredis'
|
|
2
|
-
import RedisMock from 'ioredis-mock'
|
|
3
|
-
import { once } from 'stream'
|
|
4
|
-
|
|
5
|
-
function createClient(num: number) {
|
|
6
|
-
const serverURL =
|
|
7
|
-
process.env[`REDIS_URI${num}`] || `redis://127.0.0.1:${6000 + num}`
|
|
8
|
-
const client = new Redis(serverURL, {
|
|
9
|
-
connectionName: `client${num}`,
|
|
10
|
-
lazyConnect: true,
|
|
11
|
-
enableOfflineQueue: false,
|
|
12
|
-
autoResendUnfulfilledCommands: false, // dont queue commands while server is offline (dont break test logic)
|
|
13
|
-
maxRetriesPerRequest: 0, // dont retry, fail faster (default is 20)
|
|
14
|
-
|
|
15
|
-
// https://github.com/luin/ioredis#auto-reconnect
|
|
16
|
-
// retryStrategy is a function that will be called when the connection is lost.
|
|
17
|
-
// The argument times means this is the nth reconnection being made and the return value represents how long (in ms) to wait to reconnect.
|
|
18
|
-
retryStrategy() {
|
|
19
|
-
return 100 // for tests we disable increasing timeout
|
|
20
|
-
}
|
|
21
|
-
})
|
|
22
|
-
client.on('error', err => {
|
|
23
|
-
console.log('Redis client error:', err.message)
|
|
24
|
-
})
|
|
25
|
-
return client
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function createClientMock(num: number) {
|
|
29
|
-
return new RedisMock(`redis://mock:${4200 + num}`, {
|
|
30
|
-
connectionName: `client-mock${num}`,
|
|
31
|
-
lazyConnect: true,
|
|
32
|
-
enableOfflineQueue: false,
|
|
33
|
-
autoResendUnfulfilledCommands: false, // dont queue commands while server is offline (dont break test logic)
|
|
34
|
-
maxRetriesPerRequest: 0, // dont retry, fail faster (default is 20)
|
|
35
|
-
|
|
36
|
-
// https://github.com/luin/ioredis#auto-reconnect
|
|
37
|
-
// retryStrategy is a function that will be called when the connection is lost.
|
|
38
|
-
// The argument times means this is the nth reconnection being made and the return value represents how long (in ms) to wait to reconnect.
|
|
39
|
-
retryStrategy() {
|
|
40
|
-
return 100 // for tests we disable increasing timeout
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export const client1 = createClient(1)
|
|
46
|
-
export const client2 = createClient(2)
|
|
47
|
-
export const client3 = createClient(3)
|
|
48
|
-
|
|
49
|
-
export const allClients = [client1, client2, client3]
|
|
50
|
-
|
|
51
|
-
export const clientMock1 = createClientMock(1)
|
|
52
|
-
export const clientMock2 = createClientMock(2)
|
|
53
|
-
export const clientMock3 = createClientMock(3)
|
|
54
|
-
|
|
55
|
-
export const allClientMocks = [clientMock1, clientMock2, clientMock3]
|
|
56
|
-
|
|
57
|
-
before(async () => {
|
|
58
|
-
await Promise.all(allClients.map(c => c.connect()))
|
|
59
|
-
await Promise.all(allClientMocks.map(c => c.connect()))
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
beforeEach(async () => {
|
|
63
|
-
await Promise.all(
|
|
64
|
-
allClients.map(c => {
|
|
65
|
-
if (c.status !== 'ready') {
|
|
66
|
-
console.warn(
|
|
67
|
-
`client ${c.options.connectionName} status = ${c.status}. Wait for ready.`
|
|
68
|
-
)
|
|
69
|
-
return once(c, 'ready')
|
|
70
|
-
}
|
|
71
|
-
return null
|
|
72
|
-
})
|
|
73
|
-
)
|
|
74
|
-
await Promise.all(allClients.map(c => c.flushdb()))
|
|
75
|
-
await Promise.all(allClientMocks.map(c => c.flushdb()))
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
after(async () => {
|
|
79
|
-
await Promise.all(allClients.map(c => c.quit()))
|
|
80
|
-
await Promise.all(allClientMocks.map(c => c.quit()))
|
|
81
|
-
// allClients.forEach(c => c.disconnect())
|
|
82
|
-
})
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { downRedisServer, upRedisServer } from './shell'
|
|
2
|
-
|
|
3
|
-
describe('TEST UTILS', () => {
|
|
4
|
-
describe('shell', () => {
|
|
5
|
-
it('should up redis server', async function () {
|
|
6
|
-
this.timeout(30000)
|
|
7
|
-
await upRedisServer(1)
|
|
8
|
-
})
|
|
9
|
-
it('should down and up redis servers', async function () {
|
|
10
|
-
this.timeout(30000)
|
|
11
|
-
await downRedisServer(1)
|
|
12
|
-
await upRedisServer(1)
|
|
13
|
-
})
|
|
14
|
-
})
|
|
15
|
-
})
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { exec } from 'child_process'
|
|
2
|
-
|
|
3
|
-
import { delay } from '../src/utils/index'
|
|
4
|
-
|
|
5
|
-
const LOGGING = !!process.env.LOGSHELL
|
|
6
|
-
|
|
7
|
-
function sh(cmd: string) {
|
|
8
|
-
return new Promise<void>((resolve, reject) => {
|
|
9
|
-
const cp = exec(cmd, (err, stdout, stderr) => {
|
|
10
|
-
if (stdout && LOGGING) {
|
|
11
|
-
console.log(`[${cp.pid}] stdout:`)
|
|
12
|
-
console.log(stdout)
|
|
13
|
-
}
|
|
14
|
-
if (stderr && LOGGING) {
|
|
15
|
-
console.log(`[${cp.pid}] stderr:`)
|
|
16
|
-
console.log(stderr)
|
|
17
|
-
}
|
|
18
|
-
if (err) {
|
|
19
|
-
reject(err)
|
|
20
|
-
} else {
|
|
21
|
-
resolve()
|
|
22
|
-
}
|
|
23
|
-
})
|
|
24
|
-
console.log(`[${cp.pid}] ${cmd}`)
|
|
25
|
-
})
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function upRedisServer(num: number) {
|
|
29
|
-
const port = 6000 + num
|
|
30
|
-
await sh(
|
|
31
|
-
`docker compose up -d redis${num} && yarn wait-for --redis redis://127.0.0.1:${port}`
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function downRedisServer(num: number) {
|
|
36
|
-
const port = 6000 + num
|
|
37
|
-
await sh(`docker compose stop redis${num}`)
|
|
38
|
-
let tries = 0
|
|
39
|
-
while (true) {
|
|
40
|
-
try {
|
|
41
|
-
console.log(`wait server${num} shut down... ${++tries}`)
|
|
42
|
-
await sh(`yarn wait-for --redis redis://127.0.0.1:${port} -c 1`)
|
|
43
|
-
await delay(100)
|
|
44
|
-
} catch {
|
|
45
|
-
break
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { LockOptions } from '../../src'
|
|
2
|
-
import { Lock } from '../../src/Lock'
|
|
3
|
-
import { delay } from '../../src/utils'
|
|
4
|
-
|
|
5
|
-
describe('Lock', () => {
|
|
6
|
-
describe('refresh and release race condition', () => {
|
|
7
|
-
class TestLock extends Lock {
|
|
8
|
-
protected _kind = 'test-lock'
|
|
9
|
-
protected _key: string
|
|
10
|
-
constructor(key: string, options: LockOptions) {
|
|
11
|
-
super(options)
|
|
12
|
-
this._key = key
|
|
13
|
-
}
|
|
14
|
-
protected async _refresh(): Promise<boolean> {
|
|
15
|
-
await delay(200)
|
|
16
|
-
return false
|
|
17
|
-
}
|
|
18
|
-
protected async _acquire(): Promise<boolean> {
|
|
19
|
-
return true
|
|
20
|
-
}
|
|
21
|
-
protected async _release(): Promise<void> {}
|
|
22
|
-
}
|
|
23
|
-
it('should not throw LostLock error when refresh started but not finished before release happened', async function () {
|
|
24
|
-
const lock = new TestLock('key', {
|
|
25
|
-
lockTimeout: 1000,
|
|
26
|
-
acquireTimeout: 1000,
|
|
27
|
-
refreshInterval: 50
|
|
28
|
-
})
|
|
29
|
-
try {
|
|
30
|
-
await lock.acquire()
|
|
31
|
-
await delay(100)
|
|
32
|
-
} finally {
|
|
33
|
-
await lock.release()
|
|
34
|
-
}
|
|
35
|
-
})
|
|
36
|
-
})
|
|
37
|
-
})
|