presidium-db-client-server 0.4.0 → 0.6.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/DatabaseHandler.js +194 -161
- package/DatabaseLoadBalancer.js +95 -0
- package/DatabaseServer.js +36 -10
- package/DiskHashTableClient.js +17 -12
- package/DiskSortedHashTableClient.js +107 -76
- package/errorHandler.js +18 -0
- package/healthCheckHandler.js +18 -0
- package/loadBalancerHandler.js +40 -0
- package/notFoundHandler.js +18 -0
- package/package.json +10 -4
- package/parseString.js +18 -0
package/DatabaseHandler.js
CHANGED
|
@@ -111,186 +111,219 @@ function DatabaseHandler(options) {
|
|
|
111
111
|
request.pathSegments = request.path.slice(1).split("/")
|
|
112
112
|
request.pathDepth = request.pathSegments.length
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
})
|
|
118
|
-
response.end("OK")
|
|
119
|
-
} else if (request.pathDepth == 2) {
|
|
120
|
-
const tableName = request.pathSegments[0]
|
|
121
|
-
const fname = request.pathSegments[1]
|
|
122
|
-
const ht = hts.get(tableName)
|
|
123
|
-
?? await getHt(tableName, request, manifestHt)
|
|
114
|
+
const tableName = request.pathSegments[0]
|
|
115
|
+
const fname = request.pathSegments[1]
|
|
116
|
+
const ht = hts.get(tableName) ?? await getHt(tableName, request, manifestHt)
|
|
124
117
|
|
|
125
|
-
|
|
126
|
-
|
|
118
|
+
await switchCase(
|
|
119
|
+
fname,
|
|
127
120
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
121
|
+
eq("clear"), async () => {
|
|
122
|
+
await ht.clear()
|
|
123
|
+
response.writeHead(200, {
|
|
124
|
+
"Content-Type": "text/plain",
|
|
125
|
+
})
|
|
126
|
+
response.end("OK")
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
eq("set"), async () => {
|
|
130
|
+
const key = request.headers["key"]
|
|
131
|
+
const sortValue = request.headers["sort-value"]
|
|
132
|
+
const value = request.headers["content-type"] == "text/plain"
|
|
133
|
+
? await Readable.Text(request)
|
|
134
|
+
: await Readable.Buffer(request)
|
|
135
|
+
await ht.set(key, value, sortValue)
|
|
136
|
+
response.writeHead(200, {
|
|
137
|
+
"Content-Type": "text/plain",
|
|
138
|
+
})
|
|
139
|
+
response.end("OK")
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
eq("get"), async () => {
|
|
143
|
+
const key = request.headers["key"]
|
|
144
|
+
const value = await ht.get(key)
|
|
145
|
+
if (value === undefined) {
|
|
146
|
+
response.writeHead(499, {
|
|
131
147
|
"Content-Type": "text/plain",
|
|
132
148
|
})
|
|
133
|
-
response.end("
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
eq("set"), async () => {
|
|
137
|
-
const { key, value, sortValue } = await Readable.JSON(request)
|
|
138
|
-
await ht.set(key, value, sortValue)
|
|
149
|
+
response.end("Resource Not Found")
|
|
150
|
+
} else {
|
|
139
151
|
response.writeHead(200, {
|
|
140
152
|
"Content-Type": "text/plain",
|
|
141
153
|
})
|
|
142
|
-
response.end(
|
|
143
|
-
}
|
|
154
|
+
response.end(value)
|
|
155
|
+
}
|
|
156
|
+
},
|
|
144
157
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
} else {
|
|
154
|
-
response.writeHead(200, {
|
|
155
|
-
"Content-Type": "text/plain",
|
|
156
|
-
})
|
|
157
|
-
response.end(value)
|
|
158
|
-
}
|
|
159
|
-
},
|
|
158
|
+
eq("getBinary"), async () => {
|
|
159
|
+
const key = request.headers["key"]
|
|
160
|
+
const value = await ht.getBinary(key)
|
|
161
|
+
response.writeHead(200, {
|
|
162
|
+
"Content-Type": "application/octet-stream",
|
|
163
|
+
})
|
|
164
|
+
response.end(value)
|
|
165
|
+
},
|
|
160
166
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
eq("delete"), async () => {
|
|
168
|
+
const key = request.headers["key"]
|
|
169
|
+
const didDelete = await ht.delete(key)
|
|
170
|
+
response.writeHead(200, {
|
|
171
|
+
"Content-Type": "text/plain",
|
|
172
|
+
})
|
|
173
|
+
response.end(String(didDelete))
|
|
174
|
+
},
|
|
169
175
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
},
|
|
176
|
+
eq("count"), async () => {
|
|
177
|
+
const count = ht.count()
|
|
178
|
+
response.writeHead(200, {
|
|
179
|
+
"Content-Type": "text/plain",
|
|
180
|
+
})
|
|
181
|
+
response.end(String(count))
|
|
182
|
+
},
|
|
178
183
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
184
|
+
eq("iterator"), async () => {
|
|
185
|
+
const valueType = request.headers["value-type"]
|
|
186
|
+
const options = filter({ valueType }, __)
|
|
187
|
+
response.writeHead(200, {
|
|
188
|
+
"Content-Type": "application/octet-stream",
|
|
189
|
+
"Content-Encoding": "utf8",
|
|
190
|
+
})
|
|
191
|
+
const count = ht.count()
|
|
192
|
+
for await (const value of ht.iterator(options)) {
|
|
193
|
+
response.write(value)
|
|
194
|
+
response.write("")
|
|
195
|
+
}
|
|
196
|
+
response.end()
|
|
197
|
+
},
|
|
186
198
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
response.
|
|
199
|
-
|
|
199
|
+
eq("entriesIterator"), async () => {
|
|
200
|
+
const valueType = request.headers["value-type"]
|
|
201
|
+
const options = filter({ valueType }, __)
|
|
202
|
+
response.writeHead(200, {
|
|
203
|
+
"Content-Type": "application/octet-stream",
|
|
204
|
+
"Content-Encoding": "utf8",
|
|
205
|
+
})
|
|
206
|
+
const count = ht.count()
|
|
207
|
+
for await (const [key, value] of ht.entriesIterator(options)) {
|
|
208
|
+
response.write(key)
|
|
209
|
+
response.write("")
|
|
210
|
+
response.write(value)
|
|
211
|
+
response.write("")
|
|
212
|
+
}
|
|
213
|
+
response.end()
|
|
214
|
+
},
|
|
200
215
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
response.write("")
|
|
213
|
-
}
|
|
214
|
-
response.end()
|
|
215
|
-
},
|
|
216
|
+
eq("forwardIterator"), async () => {
|
|
217
|
+
const valueType = request.headers["value-type"]
|
|
218
|
+
const exclusiveStartKey = request.headers["exclusive-start-key"]
|
|
219
|
+
const startingSortValue = request.headers["starting-sort-value"]
|
|
220
|
+
const endingSortValue = request.headers["ending-sort-value"]
|
|
221
|
+
const options = filter({
|
|
222
|
+
valueType,
|
|
223
|
+
exclusiveStartKey,
|
|
224
|
+
startingSortValue,
|
|
225
|
+
endingSortValue,
|
|
226
|
+
}, __)
|
|
216
227
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
response.end()
|
|
229
|
-
},
|
|
228
|
+
response.writeHead(200, {
|
|
229
|
+
"Content-Type": "application/octet-stream",
|
|
230
|
+
"Content-Encoding": "utf8",
|
|
231
|
+
})
|
|
232
|
+
const count = ht.count()
|
|
233
|
+
for await (const value of ht.forwardIterator(options)) {
|
|
234
|
+
response.write(value)
|
|
235
|
+
response.write("")
|
|
236
|
+
}
|
|
237
|
+
response.end()
|
|
238
|
+
},
|
|
230
239
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
response.end()
|
|
243
|
-
},
|
|
240
|
+
eq("reverseIterator"), async () => {
|
|
241
|
+
const valueType = request.headers["value-type"]
|
|
242
|
+
const exclusiveStartKey = request.headers["exclusive-start-key"]
|
|
243
|
+
const startingSortValue = request.headers["starting-sort-value"]
|
|
244
|
+
const endingSortValue = request.headers["ending-sort-value"]
|
|
245
|
+
const options = filter({
|
|
246
|
+
valueType,
|
|
247
|
+
exclusiveStartKey,
|
|
248
|
+
startingSortValue,
|
|
249
|
+
endingSortValue,
|
|
250
|
+
}, __)
|
|
244
251
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
response.write("")
|
|
257
|
-
response.write(String(sortValue))
|
|
258
|
-
response.write("")
|
|
259
|
-
}
|
|
260
|
-
response.end()
|
|
261
|
-
},
|
|
252
|
+
response.writeHead(200, {
|
|
253
|
+
"Content-Type": "application/octet-stream",
|
|
254
|
+
"Content-Encoding": "utf8",
|
|
255
|
+
})
|
|
256
|
+
const count = ht.count()
|
|
257
|
+
for await (const value of ht.reverseIterator(options)) {
|
|
258
|
+
response.write(value)
|
|
259
|
+
response.write("")
|
|
260
|
+
}
|
|
261
|
+
response.end()
|
|
262
|
+
},
|
|
262
263
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
response.write("")
|
|
275
|
-
response.write(String(sortValue))
|
|
276
|
-
response.write("")
|
|
277
|
-
}
|
|
278
|
-
response.end()
|
|
279
|
-
},
|
|
264
|
+
eq("forwardEntriesIterator"), async () => {
|
|
265
|
+
const valueType = request.headers["value-type"]
|
|
266
|
+
const exclusiveStartKey = request.headers["exclusive-start-key"]
|
|
267
|
+
const startingSortValue = request.headers["starting-sort-value"]
|
|
268
|
+
const endingSortValue = request.headers["ending-sort-value"]
|
|
269
|
+
const options = filter({
|
|
270
|
+
valueType,
|
|
271
|
+
exclusiveStartKey,
|
|
272
|
+
startingSortValue,
|
|
273
|
+
endingSortValue,
|
|
274
|
+
}, __)
|
|
280
275
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
276
|
+
response.writeHead(200, {
|
|
277
|
+
"Content-Type": "application/octet-stream",
|
|
278
|
+
"Content-Encoding": "utf8",
|
|
279
|
+
})
|
|
280
|
+
const count = ht.count()
|
|
281
|
+
for await (const [key, value, sortValue] of ht.forwardEntriesIterator(options)) {
|
|
282
|
+
response.write(key)
|
|
283
|
+
response.write("")
|
|
284
|
+
response.write(value)
|
|
285
|
+
response.write("")
|
|
286
|
+
response.write(String(sortValue))
|
|
287
|
+
response.write("")
|
|
288
|
+
}
|
|
289
|
+
response.end()
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
eq("reverseEntriesIterator"), async () => {
|
|
293
|
+
const valueType = request.headers["value-type"]
|
|
294
|
+
const exclusiveStartKey = request.headers["exclusive-start-key"]
|
|
295
|
+
const startingSortValue = request.headers["starting-sort-value"]
|
|
296
|
+
const endingSortValue = request.headers["ending-sort-value"]
|
|
297
|
+
const options = filter({
|
|
298
|
+
valueType,
|
|
299
|
+
exclusiveStartKey,
|
|
300
|
+
startingSortValue,
|
|
301
|
+
endingSortValue,
|
|
302
|
+
}, __)
|
|
303
|
+
|
|
304
|
+
response.writeHead(200, {
|
|
305
|
+
"Content-Type": "application/octet-stream",
|
|
306
|
+
"Content-Encoding": "utf8",
|
|
307
|
+
})
|
|
308
|
+
const count = ht.count()
|
|
309
|
+
for await (const [key, value, sortValue] of ht.reverseEntriesIterator(options)) {
|
|
310
|
+
response.write(key)
|
|
311
|
+
response.write("")
|
|
312
|
+
response.write(value)
|
|
313
|
+
response.write("")
|
|
314
|
+
response.write(String(sortValue))
|
|
315
|
+
response.write("")
|
|
316
|
+
}
|
|
317
|
+
response.end()
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
async () => {
|
|
321
|
+
const error = new Error("Forbidden: Hash Table Method")
|
|
322
|
+
error.code = "403"
|
|
323
|
+
throw error
|
|
324
|
+
},
|
|
325
|
+
)
|
|
287
326
|
|
|
288
|
-
} else {
|
|
289
|
-
response.writeHead(404, {
|
|
290
|
-
"Content-Type": "text/plain",
|
|
291
|
-
})
|
|
292
|
-
response.end("Not Found")
|
|
293
|
-
}
|
|
294
327
|
}
|
|
295
328
|
}
|
|
296
329
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presidium DB Client and Server may be freely distributed under the CFOSS license.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
require("rubico/global")
|
|
6
|
+
const HTTP = require("presidium/HTTP")
|
|
7
|
+
const loadBalancerHandler = require("./loadBalancerHandler")
|
|
8
|
+
const healthCheckHandler = require("./healthCheckHandler")
|
|
9
|
+
const notFoundHandler = require("./notFoundHandler")
|
|
10
|
+
const errorHandler = require("./errorHandler")
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @name DatabaseLoadbalancer
|
|
14
|
+
*
|
|
15
|
+
* @docs
|
|
16
|
+
* ```specscript
|
|
17
|
+
* module HTTP "https://presidium.services/docs/HTTP"
|
|
18
|
+
*
|
|
19
|
+
* HTTP.Handler(HTTP.Request, HTTP.Response) -> ()
|
|
20
|
+
*
|
|
21
|
+
* new DatabaseLoadbalancer(
|
|
22
|
+
* hash(tableName string, key string, length number) -> Promise|number,
|
|
23
|
+
* options {
|
|
24
|
+
* serverIps: Array<string>,
|
|
25
|
+
* },
|
|
26
|
+
* ...handlers Arguments<HTTP.Handler>
|
|
27
|
+
* )
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
class DatabaseLoadbalancer {
|
|
31
|
+
constructor(hash, options, ...handlers) {
|
|
32
|
+
this.hash = hash
|
|
33
|
+
this.serverIps = options.serverIps
|
|
34
|
+
this.handlers = handlers
|
|
35
|
+
this.clients = new Map()
|
|
36
|
+
|
|
37
|
+
const router = tryCatch(
|
|
38
|
+
switchCase(
|
|
39
|
+
request => request.path == "/health",
|
|
40
|
+
healthCheckHandler,
|
|
41
|
+
|
|
42
|
+
request => request.pathDepth == 2,
|
|
43
|
+
curry.call(loadBalancerHandler, this),
|
|
44
|
+
|
|
45
|
+
notFoundHandler,
|
|
46
|
+
),
|
|
47
|
+
errorHandler
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const handler = all.series([
|
|
51
|
+
...this.handlers,
|
|
52
|
+
request => {
|
|
53
|
+
const { pathname } = new URL(`http://*${request.url}`)
|
|
54
|
+
request.path = decodeURIComponent(
|
|
55
|
+
pathname == "/" ? pathname
|
|
56
|
+
: pathname.endsWith("/") ? pathname.slice(0, -1)
|
|
57
|
+
: pathname
|
|
58
|
+
)
|
|
59
|
+
request.pathSegments = request.path.slice(1).split("/")
|
|
60
|
+
request.pathDepth = request.pathSegments.length
|
|
61
|
+
},
|
|
62
|
+
router,
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
this.server = HTTP.Server(handler)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @name listen
|
|
70
|
+
*
|
|
71
|
+
* @docs
|
|
72
|
+
* ```specscript
|
|
73
|
+
* listen(port number) -> ()
|
|
74
|
+
* listen(port number, callback function) -> ()
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
listen(port, callback) {
|
|
78
|
+
this.server.listen(port, callback)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @name close
|
|
83
|
+
*
|
|
84
|
+
* @docs
|
|
85
|
+
* ```specscript
|
|
86
|
+
* close() -> ()
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
close() {
|
|
90
|
+
this.server.close()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = DatabaseLoadbalancer
|
package/DatabaseServer.js
CHANGED
|
@@ -7,12 +7,19 @@ const HTTP = require("presidium/HTTP")
|
|
|
7
7
|
const DiskHashTable = require("presidium/DiskHashTable")
|
|
8
8
|
const DiskSortedHashTable = require("presidium/DiskSortedHashTable")
|
|
9
9
|
const DatabaseHandler = require("./DatabaseHandler")
|
|
10
|
+
const healthCheckHandler = require("./healthCheckHandler")
|
|
11
|
+
const notFoundHandler = require("./notFoundHandler")
|
|
12
|
+
const errorHandler = require("./errorHandler")
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* @name DatabaseServer
|
|
13
16
|
*
|
|
14
17
|
* @docs
|
|
15
18
|
* ```specscript
|
|
19
|
+
* module HTTP "https://presidium.services/docs/HTTP"
|
|
20
|
+
*
|
|
21
|
+
* HTTP.Handler(request HTTP.Request, response HTTP.Response) -> ()
|
|
22
|
+
*
|
|
16
23
|
* new DatabaseServer(options {
|
|
17
24
|
* databasePath: string,
|
|
18
25
|
* manifestTableName: string,
|
|
@@ -21,11 +28,11 @@ const DatabaseHandler = require("./DatabaseHandler")
|
|
|
21
28
|
* defaultTableInitialLength: number,
|
|
22
29
|
* defaultTableItemSize: number,
|
|
23
30
|
* defaultDiskSortedHashTableDegree: number,
|
|
24
|
-
* }) -> server DatabaseServer
|
|
31
|
+
* }, ...handlers Arguments<HTTP.Handler>) -> server DatabaseServer
|
|
25
32
|
* ```
|
|
26
33
|
*/
|
|
27
34
|
class DatabaseServer {
|
|
28
|
-
constructor(options) {
|
|
35
|
+
constructor(options, ...handlers) {
|
|
29
36
|
this.databasePath = options.databasePath
|
|
30
37
|
this.manifestTableName = options.manifestTableName ?? "Manifest"
|
|
31
38
|
this.manifestTableInitialLength = options.manifestTableInitialLength ?? 1048576
|
|
@@ -33,6 +40,7 @@ class DatabaseServer {
|
|
|
33
40
|
this.defaultTableInitialLength = options.defaultTableInitialLength ?? 1048576
|
|
34
41
|
this.defaultTableItemSize = options.defaultTableItemSize ?? 1024
|
|
35
42
|
this.defaultDiskSortedHashTableDegree = options.defaultDiskSortedHashTableDegree ?? 2
|
|
43
|
+
this.handlers = handlers
|
|
36
44
|
this.server = null
|
|
37
45
|
this.hts = new Map()
|
|
38
46
|
}
|
|
@@ -67,16 +75,34 @@ class DatabaseServer {
|
|
|
67
75
|
hts: this.hts,
|
|
68
76
|
})
|
|
69
77
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
const router = tryCatch(
|
|
79
|
+
switchCase(
|
|
80
|
+
request => request.path == "/health",
|
|
81
|
+
healthCheckHandler,
|
|
82
|
+
|
|
83
|
+
request => request.pathDepth == 2,
|
|
84
|
+
databaseHandler,
|
|
85
|
+
|
|
86
|
+
notFoundHandler,
|
|
87
|
+
),
|
|
88
|
+
errorHandler
|
|
78
89
|
)
|
|
79
90
|
|
|
91
|
+
const handler = all.series([
|
|
92
|
+
...this.handlers,
|
|
93
|
+
request => {
|
|
94
|
+
const { pathname } = new URL(`http://*${request.url}`)
|
|
95
|
+
request.path = decodeURIComponent(
|
|
96
|
+
pathname == "/" ? pathname
|
|
97
|
+
: pathname.endsWith("/") ? pathname.slice(0, -1)
|
|
98
|
+
: pathname
|
|
99
|
+
)
|
|
100
|
+
request.pathSegments = request.path.slice(1).split("/")
|
|
101
|
+
request.pathDepth = request.pathSegments.length
|
|
102
|
+
},
|
|
103
|
+
router,
|
|
104
|
+
])
|
|
105
|
+
|
|
80
106
|
this.server = HTTP.Server(handler)
|
|
81
107
|
}
|
|
82
108
|
|
package/DiskHashTableClient.js
CHANGED
|
@@ -41,6 +41,7 @@ class DiskHashTableClient {
|
|
|
41
41
|
async clear() {
|
|
42
42
|
const response = await this.http.POST(`/${this.tableName}/clear`, {
|
|
43
43
|
headers: {
|
|
44
|
+
"Content-Type": "text/plain",
|
|
44
45
|
"Client-Name": "DiskHashTableClient",
|
|
45
46
|
},
|
|
46
47
|
})
|
|
@@ -60,10 +61,13 @@ class DiskHashTableClient {
|
|
|
60
61
|
async set(key, value) {
|
|
61
62
|
const response = await this.http.POST(`/${this.tableName}/set`, {
|
|
62
63
|
headers: {
|
|
63
|
-
"Content-Type":
|
|
64
|
+
"Content-Type": Buffer.isBuffer(value)
|
|
65
|
+
? "application/octet-stream"
|
|
66
|
+
: "text/plain",
|
|
64
67
|
"Client-Name": "DiskHashTableClient",
|
|
68
|
+
"Key": key,
|
|
65
69
|
},
|
|
66
|
-
body:
|
|
70
|
+
body: value,
|
|
67
71
|
})
|
|
68
72
|
await handleServerResponse(response)
|
|
69
73
|
}
|
|
@@ -81,10 +85,10 @@ class DiskHashTableClient {
|
|
|
81
85
|
async get(key) {
|
|
82
86
|
const response = await this.http.POST(`/${this.tableName}/get`, {
|
|
83
87
|
headers: {
|
|
84
|
-
"Content-Type": "
|
|
88
|
+
"Content-Type": "text/plain",
|
|
85
89
|
"Client-Name": "DiskHashTableClient",
|
|
90
|
+
"Key": key,
|
|
86
91
|
},
|
|
87
|
-
body: JSON.stringify({ key }),
|
|
88
92
|
})
|
|
89
93
|
if (response.status == 499) {
|
|
90
94
|
return undefined
|
|
@@ -105,10 +109,10 @@ class DiskHashTableClient {
|
|
|
105
109
|
async getBinary(key) {
|
|
106
110
|
const response = await this.http.POST(`/${this.tableName}/getBinary`, {
|
|
107
111
|
headers: {
|
|
108
|
-
"Content-Type": "
|
|
112
|
+
"Content-Type": "text/plain",
|
|
109
113
|
"Client-Name": "DiskHashTableClient",
|
|
114
|
+
"Key": key,
|
|
110
115
|
},
|
|
111
|
-
body: JSON.stringify({ key }),
|
|
112
116
|
})
|
|
113
117
|
return handleServerResponse(response)
|
|
114
118
|
}
|
|
@@ -126,10 +130,10 @@ class DiskHashTableClient {
|
|
|
126
130
|
async delete(key) {
|
|
127
131
|
const response = await this.http.POST(`/${this.tableName}/delete`, {
|
|
128
132
|
headers: {
|
|
129
|
-
"Content-Type": "
|
|
133
|
+
"Content-Type": "text/plain",
|
|
130
134
|
"Client-Name": "DiskHashTableClient",
|
|
135
|
+
"Key": key,
|
|
131
136
|
},
|
|
132
|
-
body: JSON.stringify({ key }),
|
|
133
137
|
})
|
|
134
138
|
const didDeleteText = await handleServerResponse(response)
|
|
135
139
|
return didDeleteText == "true"
|
|
@@ -146,6 +150,7 @@ class DiskHashTableClient {
|
|
|
146
150
|
async count() {
|
|
147
151
|
const response = await this.http.POST(`/${this.tableName}/count`, {
|
|
148
152
|
headers: {
|
|
153
|
+
"Content-Type": "text/plain",
|
|
149
154
|
"Client-Name": "DiskHashTableClient",
|
|
150
155
|
},
|
|
151
156
|
})
|
|
@@ -167,10 +172,10 @@ class DiskHashTableClient {
|
|
|
167
172
|
const { valueType } = options
|
|
168
173
|
const response = await this.http.POST(`/${this.tableName}/iterator`, {
|
|
169
174
|
headers: {
|
|
170
|
-
"Content-Type": "
|
|
175
|
+
"Content-Type": "text/plain",
|
|
171
176
|
"Client-Name": "DiskHashTableClient",
|
|
177
|
+
...valueType ? { "Value-Type": valueType } : {},
|
|
172
178
|
},
|
|
173
|
-
body: JSON.stringify(options),
|
|
174
179
|
})
|
|
175
180
|
if (valueType == "binary") {
|
|
176
181
|
return handleServerResponse(response, {
|
|
@@ -198,10 +203,10 @@ class DiskHashTableClient {
|
|
|
198
203
|
const { valueType } = options
|
|
199
204
|
const response = await this.http.POST(`/${this.tableName}/entriesIterator`, {
|
|
200
205
|
headers: {
|
|
201
|
-
"Content-Type": "
|
|
206
|
+
"Content-Type": "text/plain",
|
|
202
207
|
"Client-Name": "DiskHashTableClient",
|
|
208
|
+
...valueType ? { "Value-Type": valueType } : {},
|
|
203
209
|
},
|
|
204
|
-
body: JSON.stringify({ valueType }),
|
|
205
210
|
})
|
|
206
211
|
|
|
207
212
|
if (valueType == "binary") {
|
|
@@ -6,6 +6,7 @@ require("rubico/global")
|
|
|
6
6
|
const HTTP = require("presidium/HTTP")
|
|
7
7
|
const handleServerResponse = require("./handleServerResponse")
|
|
8
8
|
const buffersToEntry = require("./buffersToEntry")
|
|
9
|
+
const parseString = require("./parseString")
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @name DiskSortedHashTableClient
|
|
@@ -44,13 +45,14 @@ class DiskSortedHashTableClient {
|
|
|
44
45
|
async clear() {
|
|
45
46
|
const response = await this.http.POST(`/${this.tableName}/clear`, {
|
|
46
47
|
headers: {
|
|
48
|
+
"Content-Type": "text/plain",
|
|
47
49
|
"Client-Name": "DiskSortedHashTableClient",
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
|
|
51
|
+
...pipe(this, all({
|
|
52
|
+
"Sort-Value-Type": get("sortValueType"),
|
|
53
|
+
"Degree": get("degree"),
|
|
54
|
+
}), filter(__), map(parseString)),
|
|
55
|
+
|
|
54
56
|
},
|
|
55
57
|
})
|
|
56
58
|
return handleServerResponse(response)
|
|
@@ -69,16 +71,20 @@ class DiskSortedHashTableClient {
|
|
|
69
71
|
async set(key, value, sortValue) {
|
|
70
72
|
const response = await this.http.POST(`/${this.tableName}/set`, {
|
|
71
73
|
headers: {
|
|
72
|
-
"Content-Type":
|
|
74
|
+
"Content-Type": Buffer.isBuffer(value)
|
|
75
|
+
? "application/octet-stream"
|
|
76
|
+
: "text/plain",
|
|
73
77
|
"Client-Name": "DiskSortedHashTableClient",
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
...this
|
|
78
|
-
"
|
|
79
|
-
|
|
78
|
+
"Key": key,
|
|
79
|
+
"Sort-Value": sortValue,
|
|
80
|
+
|
|
81
|
+
...pipe(this, all({
|
|
82
|
+
"Sort-Value-Type": get("sortValueType"),
|
|
83
|
+
"Degree": get("degree"),
|
|
84
|
+
}), filter(__), map(parseString)),
|
|
85
|
+
|
|
80
86
|
},
|
|
81
|
-
body:
|
|
87
|
+
body: value,
|
|
82
88
|
})
|
|
83
89
|
await handleServerResponse(response)
|
|
84
90
|
}
|
|
@@ -96,16 +102,16 @@ class DiskSortedHashTableClient {
|
|
|
96
102
|
async get(key) {
|
|
97
103
|
const response = await this.http.POST(`/${this.tableName}/get`, {
|
|
98
104
|
headers: {
|
|
99
|
-
"Content-Type": "
|
|
105
|
+
"Content-Type": "text/plain",
|
|
100
106
|
"Client-Name": "DiskSortedHashTableClient",
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"Degree":
|
|
106
|
-
}
|
|
107
|
+
"Key": key,
|
|
108
|
+
|
|
109
|
+
...pipe(this, all({
|
|
110
|
+
"Sort-Value-Type": get("sortValueType"),
|
|
111
|
+
"Degree": get("degree"),
|
|
112
|
+
}), filter(__), map(parseString)),
|
|
113
|
+
|
|
107
114
|
},
|
|
108
|
-
body: JSON.stringify({ key }),
|
|
109
115
|
})
|
|
110
116
|
if (response.status == 499) {
|
|
111
117
|
return undefined
|
|
@@ -126,16 +132,16 @@ class DiskSortedHashTableClient {
|
|
|
126
132
|
async getBinary(key) {
|
|
127
133
|
const response = await this.http.POST(`/${this.tableName}/getBinary`, {
|
|
128
134
|
headers: {
|
|
129
|
-
"Content-Type": "
|
|
135
|
+
"Content-Type": "text/plain",
|
|
130
136
|
"Client-Name": "DiskSortedHashTableClient",
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
"Degree":
|
|
136
|
-
}
|
|
137
|
+
"Key": key,
|
|
138
|
+
|
|
139
|
+
...pipe(this, all({
|
|
140
|
+
"Sort-Value-Type": get("sortValueType"),
|
|
141
|
+
"Degree": get("degree"),
|
|
142
|
+
}), filter(__), map(parseString)),
|
|
143
|
+
|
|
137
144
|
},
|
|
138
|
-
body: JSON.stringify({ key }),
|
|
139
145
|
})
|
|
140
146
|
return handleServerResponse(response)
|
|
141
147
|
}
|
|
@@ -157,16 +163,22 @@ class DiskSortedHashTableClient {
|
|
|
157
163
|
const { valueType } = options
|
|
158
164
|
const response = await this.http.POST(`/${this.tableName}/forwardIterator`, {
|
|
159
165
|
headers: {
|
|
160
|
-
"Content-Type": "
|
|
166
|
+
"Content-Type": "text/plain",
|
|
161
167
|
"Client-Name": "DiskSortedHashTableClient",
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"
|
|
167
|
-
|
|
168
|
+
|
|
169
|
+
...pipe(options, all({
|
|
170
|
+
"Exclusive-Start-Key": get("exclusiveStartKey"),
|
|
171
|
+
"Starting-Sort-Value": get("startingSortValue"),
|
|
172
|
+
"Ending-Sort-Value": get("endingSortValue"),
|
|
173
|
+
"Value-Type": get("valueType"),
|
|
174
|
+
}), filter(__), map(parseString)),
|
|
175
|
+
|
|
176
|
+
...pipe(this, all({
|
|
177
|
+
"Sort-Value-Type": get("sortValueType"),
|
|
178
|
+
"Degree": get("degree"),
|
|
179
|
+
}), filter(__), map(parseString)),
|
|
180
|
+
|
|
168
181
|
},
|
|
169
|
-
body: JSON.stringify(options),
|
|
170
182
|
})
|
|
171
183
|
if (valueType == "binary") {
|
|
172
184
|
return handleServerResponse(response, {
|
|
@@ -197,16 +209,22 @@ class DiskSortedHashTableClient {
|
|
|
197
209
|
const { valueType } = options
|
|
198
210
|
const response = await this.http.POST(`/${this.tableName}/reverseIterator`, {
|
|
199
211
|
headers: {
|
|
200
|
-
"Content-Type": "
|
|
212
|
+
"Content-Type": "text/plain",
|
|
201
213
|
"Client-Name": "DiskSortedHashTableClient",
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
"
|
|
207
|
-
|
|
214
|
+
|
|
215
|
+
...pipe(options, all({
|
|
216
|
+
"Exclusive-Start-Key": get("exclusiveStartKey"),
|
|
217
|
+
"Starting-Sort-Value": get("startingSortValue"),
|
|
218
|
+
"Ending-Sort-Value": get("endingSortValue"),
|
|
219
|
+
"Value-Type": get("valueType"),
|
|
220
|
+
}), filter(__), map(parseString)),
|
|
221
|
+
|
|
222
|
+
...pipe(this, all({
|
|
223
|
+
"Sort-Value-Type": get("sortValueType"),
|
|
224
|
+
"Degree": get("degree"),
|
|
225
|
+
}), filter(__), map(parseString)),
|
|
226
|
+
|
|
208
227
|
},
|
|
209
|
-
body: JSON.stringify(options),
|
|
210
228
|
})
|
|
211
229
|
if (valueType == "binary") {
|
|
212
230
|
return handleServerResponse(response, {
|
|
@@ -237,16 +255,22 @@ class DiskSortedHashTableClient {
|
|
|
237
255
|
const { valueType } = options
|
|
238
256
|
const response = await this.http.POST(`/${this.tableName}/forwardEntriesIterator`, {
|
|
239
257
|
headers: {
|
|
240
|
-
"Content-Type": "
|
|
258
|
+
"Content-Type": "text/plain",
|
|
241
259
|
"Client-Name": "DiskSortedHashTableClient",
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
"
|
|
247
|
-
|
|
260
|
+
|
|
261
|
+
...pipe(options, all({
|
|
262
|
+
"Exclusive-Start-Key": get("exclusiveStartKey"),
|
|
263
|
+
"Starting-Sort-Value": get("startingSortValue"),
|
|
264
|
+
"Ending-Sort-Value": get("endingSortValue"),
|
|
265
|
+
"Value-Type": get("valueType"),
|
|
266
|
+
}), filter(__), map(parseString)),
|
|
267
|
+
|
|
268
|
+
...pipe(this, all({
|
|
269
|
+
"Sort-Value-Type": get("sortValueType"),
|
|
270
|
+
"Degree": get("degree"),
|
|
271
|
+
}), filter(__), map(parseString)),
|
|
272
|
+
|
|
248
273
|
},
|
|
249
|
-
body: JSON.stringify(options),
|
|
250
274
|
})
|
|
251
275
|
if (valueType == "binary") {
|
|
252
276
|
const iter = await handleServerResponse(response, {
|
|
@@ -278,16 +302,22 @@ class DiskSortedHashTableClient {
|
|
|
278
302
|
const { valueType } = options
|
|
279
303
|
const response = await this.http.POST(`/${this.tableName}/reverseEntriesIterator`, {
|
|
280
304
|
headers: {
|
|
281
|
-
"Content-Type": "
|
|
305
|
+
"Content-Type": "text/plain",
|
|
282
306
|
"Client-Name": "DiskSortedHashTableClient",
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
"
|
|
288
|
-
|
|
307
|
+
|
|
308
|
+
...pipe(options, all({
|
|
309
|
+
"Exclusive-Start-Key": get("exclusiveStartKey"),
|
|
310
|
+
"Starting-Sort-Value": get("startingSortValue"),
|
|
311
|
+
"Ending-Sort-Value": get("endingSortValue"),
|
|
312
|
+
"Value-Type": get("valueType"),
|
|
313
|
+
}), filter(__), map(parseString)),
|
|
314
|
+
|
|
315
|
+
...pipe(this, all({
|
|
316
|
+
"Sort-Value-Type": get("sortValueType"),
|
|
317
|
+
"Degree": get("degree"),
|
|
318
|
+
}), filter(__), map(parseString)),
|
|
319
|
+
|
|
289
320
|
},
|
|
290
|
-
body: JSON.stringify(options),
|
|
291
321
|
})
|
|
292
322
|
|
|
293
323
|
if (valueType == "binary") {
|
|
@@ -319,16 +349,16 @@ class DiskSortedHashTableClient {
|
|
|
319
349
|
async delete(key) {
|
|
320
350
|
const response = await this.http.POST(`/${this.tableName}/delete`, {
|
|
321
351
|
headers: {
|
|
322
|
-
"Content-Type": "
|
|
352
|
+
"Content-Type": "text/plain",
|
|
323
353
|
"Client-Name": "DiskSortedHashTableClient",
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
"Degree":
|
|
329
|
-
}
|
|
354
|
+
"Key": key,
|
|
355
|
+
|
|
356
|
+
...pipe(this, all({
|
|
357
|
+
"Sort-Value-Type": get("sortValueType"),
|
|
358
|
+
"Degree": get("degree"),
|
|
359
|
+
}), filter(__), map(parseString)),
|
|
360
|
+
|
|
330
361
|
},
|
|
331
|
-
body: JSON.stringify({ key }),
|
|
332
362
|
})
|
|
333
363
|
const didDeleteText = await handleServerResponse(response)
|
|
334
364
|
return didDeleteText == "true"
|
|
@@ -345,13 +375,14 @@ class DiskSortedHashTableClient {
|
|
|
345
375
|
async count() {
|
|
346
376
|
const response = await this.http.POST(`/${this.tableName}/count`, {
|
|
347
377
|
headers: {
|
|
378
|
+
"Content-Type": "text/plain",
|
|
348
379
|
"Client-Name": "DiskSortedHashTableClient",
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
380
|
+
|
|
381
|
+
...pipe(this, all({
|
|
382
|
+
"Sort-Value-Type": get("sortValueType"),
|
|
383
|
+
"Degree": get("degree"),
|
|
384
|
+
}), filter(__), map(parseString)),
|
|
385
|
+
|
|
355
386
|
},
|
|
356
387
|
})
|
|
357
388
|
return Number(await handleServerResponse(response))
|
package/errorHandler.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @name errorHandler
|
|
3
|
+
*
|
|
4
|
+
* @docs
|
|
5
|
+
* ```specscript
|
|
6
|
+
* module HTTP "https://presidium.services/docs/HTTP"
|
|
7
|
+
*
|
|
8
|
+
* errorHandler(error Error, request HTTP.Request, response HTTP.Response) -> ()
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
function errorHandler(error, request, response) {
|
|
12
|
+
response.writeHead(error.code ?? 500, {
|
|
13
|
+
"Content-Type": "text/plain",
|
|
14
|
+
})
|
|
15
|
+
response.end(error.message)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = errorHandler
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @name healthCheckHandler
|
|
3
|
+
*
|
|
4
|
+
* @docs
|
|
5
|
+
* ```specscript
|
|
6
|
+
* module HTTP "https://presidium.services/docs/HTTP"
|
|
7
|
+
*
|
|
8
|
+
* healthCheckHandler(request HTTP.Request, response HTTP.Response) -> ()
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
function healthCheckHandler(request, response) {
|
|
12
|
+
response.writeHead(200, {
|
|
13
|
+
'Content-Type': 'text/plain',
|
|
14
|
+
})
|
|
15
|
+
response.end('OK')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = healthCheckHandler
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const HTTP = require("presidium/HTTP")
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @name loadBalancerHandler
|
|
5
|
+
*
|
|
6
|
+
* @docs
|
|
7
|
+
* ```specscript
|
|
8
|
+
* module HTTP "https://presidium.services/docs/HTTP"
|
|
9
|
+
*
|
|
10
|
+
* loadBalancerHandler(HTTP.Request, HTTP.Response) -> ()
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
async function loadBalancerHandler(request, response) {
|
|
14
|
+
const tableName = request.pathSegments[0]
|
|
15
|
+
const key = request.headers["key"]
|
|
16
|
+
|
|
17
|
+
let index = this.hash(tableName, key)
|
|
18
|
+
if (typeof index.then == "function") {
|
|
19
|
+
index = await index
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const serverIp = this.serverIps[index]
|
|
23
|
+
let client
|
|
24
|
+
if (this.clients.has(serverIp)) {
|
|
25
|
+
client = this.clients.get(serverIp)
|
|
26
|
+
} else {
|
|
27
|
+
client = new HTTP(serverIp)
|
|
28
|
+
this.clients.set(serverIp, client)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const serverResponse = await client[request.method](request.url, {
|
|
32
|
+
headers: request.headers,
|
|
33
|
+
body: request,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
response.writeHead(serverResponse.status, serverResponse.headers)
|
|
37
|
+
serverResponse.pipe(response)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = loadBalancerHandler
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @name notFoundHandler
|
|
3
|
+
*
|
|
4
|
+
* @docs
|
|
5
|
+
* ```specscript
|
|
6
|
+
* module HTTP "https://presidium.services/docs/HTTP"
|
|
7
|
+
*
|
|
8
|
+
* notFoundHandler(request HTTP.Request, response HTTP.Response) -> ()
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
function notFoundHandler(request, response) {
|
|
12
|
+
response.writeHead(404, {
|
|
13
|
+
"Content-Type": "text/plain",
|
|
14
|
+
})
|
|
15
|
+
response.end("Not Found")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = notFoundHandler
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "presidium-db-client-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Presidium DB client and server library",
|
|
5
5
|
"author": "Richard Tong",
|
|
6
6
|
"license": "CFOSS",
|
|
@@ -14,14 +14,20 @@
|
|
|
14
14
|
"AsyncIterator.js",
|
|
15
15
|
"BinaryEntriesStream.js",
|
|
16
16
|
"BinaryStream.js",
|
|
17
|
-
"DatabaseHandler.js",
|
|
18
17
|
"DatabaseServer.js",
|
|
18
|
+
"DatabaseHandler.js",
|
|
19
|
+
"DatabaseLoadBalancer.js",
|
|
20
|
+
"loadBalancerHandler.js",
|
|
19
21
|
"DiskHashTableClient.js",
|
|
20
22
|
"DiskSortedHashTableClient.js",
|
|
21
23
|
"TextEntriesStream.js",
|
|
22
24
|
"TextStream.js",
|
|
23
25
|
"buffersToEntry.js",
|
|
24
|
-
"handleServerResponse.js"
|
|
26
|
+
"handleServerResponse.js",
|
|
27
|
+
"healthCheckHandler.js",
|
|
28
|
+
"notFoundHandler.js",
|
|
29
|
+
"errorHandler.js",
|
|
30
|
+
"parseString.js"
|
|
25
31
|
],
|
|
26
32
|
"keywords": [
|
|
27
33
|
"presidium",
|
|
@@ -39,7 +45,7 @@
|
|
|
39
45
|
},
|
|
40
46
|
"dependencies": {
|
|
41
47
|
"presidium": "^4.5.0",
|
|
42
|
-
"rubico": "^2.
|
|
48
|
+
"rubico": "^2.21.0"
|
|
43
49
|
},
|
|
44
50
|
"devDependencies": {
|
|
45
51
|
"codecov": "^3.8.2"
|
package/parseString.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const numberRegex = /^[0-9\.]+$/
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @name parseString
|
|
5
|
+
*
|
|
6
|
+
* @docs
|
|
7
|
+
* ```specscript
|
|
8
|
+
* parseString(s string) -> number|string
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
function parseString(s) {
|
|
12
|
+
if (numberRegex.test(s)) {
|
|
13
|
+
return Number(s)
|
|
14
|
+
}
|
|
15
|
+
return s
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = parseString
|