@dra2020/baseclient 1.0.11 → 1.0.14
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 +16 -5
- package/dist/all/all.d.ts +18 -2
- package/dist/all/allclient.d.ts +18 -0
- package/dist/base.js +33010 -0
- package/dist/base.js.map +1 -0
- package/dist/baseclient.js +166 -1279
- package/dist/baseclient.js.map +1 -1
- package/dist/dbabstract/all.d.ts +1 -0
- package/dist/dbabstract/db.d.ts +83 -0
- package/dist/dbdynamo/all.d.ts +1 -0
- package/dist/dbdynamo/dbdynamo.d.ts +190 -0
- package/dist/filterexpr/filterexpr.d.ts +0 -3
- package/dist/fsm/fsm.d.ts +0 -1
- package/dist/fsmfile/all.d.ts +1 -0
- package/dist/fsmfile/fsmfile.d.ts +47 -0
- package/dist/jsonstream/all.d.ts +1 -0
- package/dist/jsonstream/jsonstream.d.ts +130 -0
- package/dist/lambda/all.d.ts +1 -0
- package/dist/lambda/env.d.ts +10 -0
- package/dist/lambda/lambda.d.ts +18 -0
- package/dist/logserver/all.d.ts +5 -0
- package/dist/logserver/log.d.ts +11 -0
- package/dist/logserver/logaccum.d.ts +154 -0
- package/dist/logserver/logblob.d.ts +24 -0
- package/dist/logserver/logconcat.d.ts +55 -0
- package/dist/logserver/logkey.d.ts +28 -0
- package/dist/memsqs/all.d.ts +4 -0
- package/dist/memsqs/client.d.ts +13 -0
- package/dist/memsqs/loopback.d.ts +11 -0
- package/dist/memsqs/orderedlist.d.ts +19 -0
- package/dist/memsqs/queue.d.ts +84 -0
- package/dist/memsqs/server.d.ts +37 -0
- package/dist/ot-js/otsession.d.ts +0 -3
- package/dist/poly/union.d.ts +0 -1
- package/dist/storage/all.d.ts +4 -0
- package/dist/storage/datablob.d.ts +9 -0
- package/dist/storage/env.d.ts +10 -0
- package/dist/storage/splitsblob.d.ts +13 -0
- package/dist/storage/storage.d.ts +166 -0
- package/dist/storages3/all.d.ts +1 -0
- package/dist/storages3/s3.d.ts +62 -0
- package/docs/dbabstract.md +2 -0
- package/docs/dbdynamo.md +2 -0
- package/docs/fsmfile.md +2 -0
- package/docs/jsonstream.md +44 -0
- package/docs/lambda.md +2 -0
- package/docs/logserver.md +2 -0
- package/docs/storage.md +2 -0
- package/docs/storages3.md +2 -0
- package/lib/all/all.ts +22 -2
- package/lib/all/allclient.ts +19 -0
- package/lib/dbabstract/all.ts +1 -0
- package/lib/dbabstract/db.ts +246 -0
- package/lib/dbdynamo/all.ts +1 -0
- package/lib/dbdynamo/dbdynamo.ts +1551 -0
- package/lib/filterexpr/filterexpr.ts +5 -79
- package/lib/fsm/fsm.ts +2 -12
- package/lib/fsmfile/all.ts +1 -0
- package/lib/fsmfile/fsmfile.ts +236 -0
- package/lib/jsonstream/all.ts +1 -0
- package/lib/jsonstream/jsonstream.ts +940 -0
- package/lib/lambda/all.ts +1 -0
- package/lib/lambda/env.ts +13 -0
- package/lib/lambda/lambda.ts +120 -0
- package/lib/logserver/all.ts +5 -0
- package/lib/logserver/log.ts +565 -0
- package/lib/logserver/logaccum.ts +1445 -0
- package/lib/logserver/logblob.ts +84 -0
- package/lib/logserver/logconcat.ts +313 -0
- package/lib/logserver/logkey.ts +125 -0
- package/lib/memsqs/all.ts +4 -0
- package/lib/memsqs/client.ts +268 -0
- package/lib/memsqs/loopback.ts +64 -0
- package/lib/memsqs/orderedlist.ts +74 -0
- package/lib/memsqs/queue.ts +395 -0
- package/lib/memsqs/server.ts +262 -0
- package/lib/ot-js/otsession.ts +1 -4
- package/lib/poly/hash.ts +1 -1
- package/lib/poly/topo.ts +41 -15
- package/lib/poly/union.ts +0 -17
- package/lib/storage/all.ts +4 -0
- package/lib/storage/datablob.ts +36 -0
- package/lib/storage/env.ts +14 -0
- package/lib/storage/splitsblob.ts +63 -0
- package/lib/storage/storage.ts +604 -0
- package/lib/storages3/all.ts +1 -0
- package/lib/storages3/s3.ts +576 -0
- package/package.json +10 -9
- package/dist/geo/all.d.ts +0 -2
- package/dist/geo/geo.d.ts +0 -67
- package/dist/geo/vfeature.d.ts +0 -4
- package/docs/filterexpr.md +0 -22
- package/lib/geo/all.ts +0 -2
- package/lib/geo/geo.ts +0 -452
- package/lib/geo/vfeature.ts +0 -34
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// Node libraries
|
|
2
|
+
import * as http from 'http';
|
|
3
|
+
import * as url from 'url';
|
|
4
|
+
|
|
5
|
+
// Project libraries
|
|
6
|
+
import * as Util from '../util/all';
|
|
7
|
+
import * as JS from '../jsonstream/all';
|
|
8
|
+
|
|
9
|
+
// Local libraries
|
|
10
|
+
import * as Q from './queue';
|
|
11
|
+
|
|
12
|
+
export type SQSCallback = (err: any, result: Q.QMessages) => void;
|
|
13
|
+
|
|
14
|
+
const MaxRetries = 10;
|
|
15
|
+
|
|
16
|
+
class SQSRequest
|
|
17
|
+
{
|
|
18
|
+
httpoptions: any;
|
|
19
|
+
data: any;
|
|
20
|
+
cb: SQSCallback;
|
|
21
|
+
jsonread: JS.JSONStreamReader;
|
|
22
|
+
jsonwrite: JS.JSONStreamWriter;
|
|
23
|
+
json: any;
|
|
24
|
+
request: any;
|
|
25
|
+
response: any;
|
|
26
|
+
statusCode: number;
|
|
27
|
+
nRetries: number;
|
|
28
|
+
|
|
29
|
+
constructor(httpoptions: any, data: any, cb: SQSCallback)
|
|
30
|
+
{
|
|
31
|
+
this.httpoptions = httpoptions;
|
|
32
|
+
this.data = data;
|
|
33
|
+
this.cb = cb;
|
|
34
|
+
this.nRetries = 0;
|
|
35
|
+
this.doRequest();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
doRequest(): void
|
|
39
|
+
{
|
|
40
|
+
this.json = null;
|
|
41
|
+
this.jsonread = null;
|
|
42
|
+
this.response = null;
|
|
43
|
+
this.statusCode = 0;
|
|
44
|
+
this.request = http.request(this.httpoptions, (res: any) => {
|
|
45
|
+
this.response = res;
|
|
46
|
+
this.statusCode = res.statusCode;
|
|
47
|
+
this.jsonread = new JS.JSONStreamReader();
|
|
48
|
+
this.jsonread.on('end', (json: any) => {
|
|
49
|
+
this.json = json;
|
|
50
|
+
if (this.statusCode == 200)
|
|
51
|
+
{
|
|
52
|
+
if (this.json.error)
|
|
53
|
+
console.log(`memsqs:client: reporting server error ${this.json.error}`);
|
|
54
|
+
this.cb(this.json.error, this.json.result);
|
|
55
|
+
}
|
|
56
|
+
else
|
|
57
|
+
{
|
|
58
|
+
console.log(`memsqs:client: reporting statusCode error ${this.statusCode}`);
|
|
59
|
+
this.onError(`error status: ${this.statusCode}`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
this.jsonread.on('error', (err: any) => {
|
|
63
|
+
// gateway errors return HTML, not JSON
|
|
64
|
+
if (this.statusCode != 502 && this.statusCode != 504)
|
|
65
|
+
{
|
|
66
|
+
console.log(`memsqs:client: reporting jsonread error ${JSON.stringify(err)}`);
|
|
67
|
+
this.onError(err);
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
{
|
|
71
|
+
console.log(`memsqs:client: reporting statusCode error ${this.statusCode}`);
|
|
72
|
+
this.onRequestError(`error status: ${this.statusCode}`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
this.jsonread.start(res);
|
|
77
|
+
|
|
78
|
+
res.on('error', (err: any) => {
|
|
79
|
+
console.log(`memsqs:client: reporting response error ${JSON.stringify(err)}`);
|
|
80
|
+
this.onError((err && err.message) ? err.message : err);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
this.request.on('error', (err: any) => {
|
|
85
|
+
// Immediately fail ECONNREFUSED rather than retrying (server not available, probably restarting)
|
|
86
|
+
if (err && err.code && err.code === 'ECONNREFUSED')
|
|
87
|
+
{
|
|
88
|
+
console.log('memsqs:client: immediately fail on ECONNREFUSED');
|
|
89
|
+
this.onError((err && err.message) ? err.message : err);
|
|
90
|
+
}
|
|
91
|
+
else
|
|
92
|
+
this.onRequestError((err && err.message) ? err.message : err);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
this.request.setHeader('Content-Type', 'application/json; charset=UTF-8');
|
|
96
|
+
this.jsonwrite = new JS.JSONStreamWriter();
|
|
97
|
+
this.jsonwrite.on('error', (err: any) => {
|
|
98
|
+
console.log(`memsqs:client: encountered request error writing response ${JSON.stringify(err)}`);
|
|
99
|
+
this.onRequestError(err);
|
|
100
|
+
});
|
|
101
|
+
this.jsonwrite.start(this.data, this.request);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
onRequestError(err: any): void
|
|
105
|
+
{
|
|
106
|
+
if (this.nRetries >= MaxRetries)
|
|
107
|
+
{
|
|
108
|
+
console.log(`memsqs:client: giving up after ${this.nRetries} with error ${JSON.stringify(err)}`);
|
|
109
|
+
this.onError(err);
|
|
110
|
+
}
|
|
111
|
+
else
|
|
112
|
+
{
|
|
113
|
+
this.nRetries++;
|
|
114
|
+
this.doRequest();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onError(err: any): void
|
|
119
|
+
{
|
|
120
|
+
if (this.cb)
|
|
121
|
+
{
|
|
122
|
+
console.log(`memsqs:client: reporting error to callback: ${JSON.stringify(err)}`);
|
|
123
|
+
this.cb(err, null);
|
|
124
|
+
this.cb = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface AgentEntry
|
|
130
|
+
{
|
|
131
|
+
agent: any;
|
|
132
|
+
inUse: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class AgentPool
|
|
136
|
+
{
|
|
137
|
+
agents: AgentEntry[];
|
|
138
|
+
|
|
139
|
+
constructor()
|
|
140
|
+
{
|
|
141
|
+
this.agents = [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
alloc(): any
|
|
145
|
+
{
|
|
146
|
+
for (let i: number = 0; i < this.agents.length; i++)
|
|
147
|
+
if (! this.agents[i].inUse)
|
|
148
|
+
{
|
|
149
|
+
this.agents[i].inUse = true;
|
|
150
|
+
return this.agents[i].agent;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let ae: AgentEntry = { agent: new http.Agent( { keepAlive: true, maxSockets: 1 } ), inUse: true };
|
|
154
|
+
this.agents.push(ae);
|
|
155
|
+
return ae.agent;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
free(agent: any): void
|
|
159
|
+
{
|
|
160
|
+
for (let i: number = 0; i < this.agents.length; i++)
|
|
161
|
+
if (this.agents[i].agent === agent)
|
|
162
|
+
{
|
|
163
|
+
if (! this.agents[i].inUse)
|
|
164
|
+
throw 'Duplicate free of agent';
|
|
165
|
+
this.agents[i].inUse = false;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
throw 'Freeing unallocated agent';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export class SQSClient
|
|
174
|
+
{
|
|
175
|
+
private targeturl?: any;
|
|
176
|
+
private httpoptions?: any;
|
|
177
|
+
private agentPool?: AgentPool;
|
|
178
|
+
|
|
179
|
+
constructor(urlstring: string = Q.DefaultServerUrl)
|
|
180
|
+
{
|
|
181
|
+
this.targeturl = new url.URL(urlstring);
|
|
182
|
+
this.httpoptions = {
|
|
183
|
+
protocol: this.targeturl.protocol,
|
|
184
|
+
host: this.targeturl.hostname,
|
|
185
|
+
port: this.targeturl.port ? this.targeturl.port : Q.DefaultPort,
|
|
186
|
+
agent: null,
|
|
187
|
+
method: 'POST',
|
|
188
|
+
path: '/',
|
|
189
|
+
};
|
|
190
|
+
this.agentPool = new AgentPool();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
setOptions(queueid: string, options: Q.QQueueOptions, cb: SQSCallback): void
|
|
194
|
+
{
|
|
195
|
+
// Use separate agent for each outstanding call
|
|
196
|
+
let httpoptions: any = Util.shallowAssignImmutable(this.httpoptions, { agent: this.agentPool.alloc() });
|
|
197
|
+
|
|
198
|
+
new SQSRequest(httpoptions,
|
|
199
|
+
{
|
|
200
|
+
queueid: queueid,
|
|
201
|
+
api: 'setoptions',
|
|
202
|
+
data: options
|
|
203
|
+
},
|
|
204
|
+
(err: any, result: Q.QMessages) => { this.agentPool.free(httpoptions.agent); cb(err, result); });
|
|
205
|
+
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
claim(queueid: string, owner: string, groupid: string, cb: SQSCallback): void
|
|
209
|
+
{
|
|
210
|
+
// Use separate agent for each outstanding call
|
|
211
|
+
let httpoptions: any = Util.shallowAssignImmutable(this.httpoptions, { agent: this.agentPool.alloc() });
|
|
212
|
+
|
|
213
|
+
new SQSRequest(httpoptions,
|
|
214
|
+
{
|
|
215
|
+
queueid: queueid,
|
|
216
|
+
api: 'claim',
|
|
217
|
+
data: { owner: owner, groupid: groupid }
|
|
218
|
+
},
|
|
219
|
+
(err: any, result: Q.QMessages) => { this.agentPool.free(httpoptions.agent); cb(err, result); });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
send(queueid: string, m: Q.QMessage, cb: SQSCallback): void
|
|
223
|
+
{
|
|
224
|
+
// Use separate agent for each outstanding call
|
|
225
|
+
let httpoptions: any = Util.shallowAssignImmutable(this.httpoptions, { agent: this.agentPool.alloc() });
|
|
226
|
+
|
|
227
|
+
new SQSRequest(httpoptions,
|
|
228
|
+
{
|
|
229
|
+
queueid: queueid,
|
|
230
|
+
api: 'send',
|
|
231
|
+
data: m
|
|
232
|
+
},
|
|
233
|
+
(err: any, result: Q.QMessages) => {
|
|
234
|
+
this.agentPool.free(httpoptions.agent);
|
|
235
|
+
if (err)
|
|
236
|
+
result = [ m ];
|
|
237
|
+
cb(err, result);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
remove(queueid: string, m: Q.QMessage, cb: SQSCallback): void
|
|
242
|
+
{
|
|
243
|
+
// Use separate agent for each outstanding call
|
|
244
|
+
let httpoptions: any = Util.shallowAssignImmutable(this.httpoptions, { agent: this.agentPool.alloc() });
|
|
245
|
+
|
|
246
|
+
new SQSRequest(httpoptions,
|
|
247
|
+
{
|
|
248
|
+
queueid: queueid,
|
|
249
|
+
api: 'remove',
|
|
250
|
+
data: m
|
|
251
|
+
},
|
|
252
|
+
(err: any, result: Q.QMessages) => { this.agentPool.free(httpoptions.agent); cb(err, result); });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
receive(queueid: string, owner: string, cb: SQSCallback): void
|
|
256
|
+
{
|
|
257
|
+
// Use separate agent for each outstanding call
|
|
258
|
+
let httpoptions: any = Util.shallowAssignImmutable(this.httpoptions, { agent: this.agentPool.alloc() });
|
|
259
|
+
|
|
260
|
+
new SQSRequest(httpoptions,
|
|
261
|
+
{
|
|
262
|
+
queueid: queueid,
|
|
263
|
+
api: 'receive',
|
|
264
|
+
owner: owner,
|
|
265
|
+
},
|
|
266
|
+
(err: any, result: Q.QMessages) => { this.agentPool.free(httpoptions.agent); cb(err, result); });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Local libraries
|
|
2
|
+
import * as Q from './queue';
|
|
3
|
+
import * as C from './client';
|
|
4
|
+
|
|
5
|
+
type MessageList = { [queueid: string]: Q.QMessage[] };
|
|
6
|
+
type ReceiveList = { [queueid: string]: C.SQSCallback };
|
|
7
|
+
|
|
8
|
+
let messageList: MessageList = {};
|
|
9
|
+
let receiveList: ReceiveList = {};
|
|
10
|
+
|
|
11
|
+
export class SQSClientLoopback
|
|
12
|
+
{
|
|
13
|
+
constructor(urlstring: string = Q.DefaultServerUrl)
|
|
14
|
+
{
|
|
15
|
+
console.log('memsqs: running in loopback mode');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setOptions(queueid: string, options: Q.QQueueOptions, cb: C.SQSCallback): void
|
|
19
|
+
{
|
|
20
|
+
cb(null, null);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
claim(queueid: string, owner: string, groupid: string, cb: C.SQSCallback): void
|
|
24
|
+
{
|
|
25
|
+
cb(null, null);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
send(queueid: string, m: Q.QMessage, cb: C.SQSCallback): void
|
|
29
|
+
{
|
|
30
|
+
let q = messageList[queueid];
|
|
31
|
+
if (q === undefined)
|
|
32
|
+
q = [], messageList[queueid] = q;
|
|
33
|
+
q.push(m);
|
|
34
|
+
cb(null, null);
|
|
35
|
+
this.doReceive();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
remove(queueid: string, m: Q.QMessage, cb: C.SQSCallback): void
|
|
39
|
+
{
|
|
40
|
+
// We just remove during receive so this is no-op
|
|
41
|
+
cb(null, null);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
receive(queueid: string, owner: string, cb: C.SQSCallback): void
|
|
45
|
+
{
|
|
46
|
+
receiveList[queueid] = cb;
|
|
47
|
+
this.doReceive();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
doReceive(): void
|
|
51
|
+
{
|
|
52
|
+
for (let queueid in receiveList) if (messageList.hasOwnProperty(queueid))
|
|
53
|
+
{
|
|
54
|
+
let q = messageList[queueid];
|
|
55
|
+
let cb = receiveList[queueid];
|
|
56
|
+
if (cb !== undefined)
|
|
57
|
+
{
|
|
58
|
+
delete messageList[queueid];
|
|
59
|
+
delete receiveList[queueid];
|
|
60
|
+
cb(null, q);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export interface OrderedListEntry<T>
|
|
2
|
+
{
|
|
3
|
+
prev: OrderedListEntry<T>;
|
|
4
|
+
next: OrderedListEntry<T>;
|
|
5
|
+
value: T;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type OrderedListIndex<T> = { [key: string]: OrderedListEntry<T> };
|
|
9
|
+
|
|
10
|
+
export class OrderedList<T>
|
|
11
|
+
{
|
|
12
|
+
head: OrderedListEntry<T>;
|
|
13
|
+
tail: OrderedListEntry<T>;
|
|
14
|
+
index: OrderedListIndex<T>;
|
|
15
|
+
|
|
16
|
+
constructor()
|
|
17
|
+
{
|
|
18
|
+
this.clear();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
isempty(): boolean
|
|
22
|
+
{
|
|
23
|
+
return this.head == null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
clear(): void
|
|
27
|
+
{
|
|
28
|
+
this.head = null;
|
|
29
|
+
this.tail = null;
|
|
30
|
+
this.index = {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
insert(key: string, value: T): string
|
|
34
|
+
{
|
|
35
|
+
if (this.index[key] !== undefined)
|
|
36
|
+
return `memsqs: send: message uid ${key} already exists`;
|
|
37
|
+
|
|
38
|
+
let e: OrderedListEntry<T> = { prev: this.tail, next: null, value: value };
|
|
39
|
+
if (this.tail)
|
|
40
|
+
this.tail.next = e;
|
|
41
|
+
this.tail = e;
|
|
42
|
+
if (this.head === null)
|
|
43
|
+
this.head = e;
|
|
44
|
+
this.index[key] = e;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
remove(key: string): string
|
|
49
|
+
{
|
|
50
|
+
let e = this.index[key];
|
|
51
|
+
if (e === undefined)
|
|
52
|
+
return `memsqs: remove: message uid ${key} does not exist`;
|
|
53
|
+
|
|
54
|
+
if (e === this.tail)
|
|
55
|
+
this.tail = e.prev;
|
|
56
|
+
else
|
|
57
|
+
e.next.prev = e.prev;
|
|
58
|
+
|
|
59
|
+
if (e === this.head)
|
|
60
|
+
this.head = e.next;
|
|
61
|
+
else
|
|
62
|
+
e.prev.next = e.next;
|
|
63
|
+
|
|
64
|
+
delete this.index[key];
|
|
65
|
+
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
forEach(cb: (value: T) => boolean): void
|
|
70
|
+
{
|
|
71
|
+
for (let p = this.head; p && cb(p.value); p = p.next)
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
}
|