presidium-db-client-server 0.0.0-stage → 0.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.
@@ -0,0 +1,17 @@
1
+ const stream = require("stream")
2
+
3
+ /**
4
+ * @name ArrayEventEmitter
5
+ *
6
+ * @docs
7
+ * ```specscript
8
+ * module EventEmitter "./specscript/EventEmitter.ss"
9
+ *
10
+ * ArrayEventEmitter(array Array<string|Buffer>) -> EventEmitter
11
+ * ```
12
+ */
13
+ function ArrayEventEmitter(array) {
14
+ return stream.Readable.from(array)
15
+ }
16
+
17
+ module.exports = ArrayEventEmitter
@@ -0,0 +1,34 @@
1
+ const AsyncIterator = {}
2
+
3
+ function sleep(time) {
4
+ return new Promise(resolve => setTimeout(resolve, time))
5
+ }
6
+
7
+ /**
8
+ * @name fromEvents
9
+ *
10
+ * @docs
11
+ * ```specscript
12
+ * module EventEmitter "./specscript/EventEmitter.ss"
13
+ *
14
+ * fromEvents(events EventEmitter) -> AsyncIterator
15
+ * fromEvents(events EventEmitter, dataEventName string) -> AsyncIterator
16
+ * ```
17
+ */
18
+ async function* fromEvents(events, dataEventName = "data") {
19
+ let buffer = []
20
+ events.on(dataEventName, data => buffer.push(data))
21
+ let streaming = true
22
+ events.on("end", () => (streaming = false))
23
+ while (streaming || buffer.length > 0) {
24
+ while (buffer.length > 0) {
25
+ yield* buffer
26
+ buffer = []
27
+ }
28
+ await sleep(1)
29
+ }
30
+ }
31
+
32
+ AsyncIterator.fromEvents = fromEvents
33
+
34
+ module.exports = AsyncIterator
@@ -0,0 +1,71 @@
1
+ const EventEmitter = require("events")
2
+
3
+ /**
4
+ * @name BinaryEntriesStream
5
+ *
6
+ * @docs
7
+ * ```coffeescript [specscript]
8
+ * module stream 'https://nodejs.org/docs/latest-v26.x/api/stream.html'
9
+ * module events 'https://nodejs.org/docs/latest-v26.x/api/events.html'
10
+ *
11
+ * BinaryEntriesStream(
12
+ * readable stream.Readable,
13
+ * entryLength number,
14
+ * delimiter string
15
+ * ) -> binaryEntriesStream events.EventEmitter
16
+ *
17
+ * binaryEntriesStream.on("entry", (entry Array<Buffer>, keyEncoding string) -> ())
18
+ *
19
+ * binaryEntriesStream.on("end", () -> ())
20
+ * ```
21
+ */
22
+ class BinaryEntriesStream extends EventEmitter {
23
+ constructor(readable, entryLength, delimiter, encoding) {
24
+ super()
25
+
26
+ delimiter ??= ""
27
+ encoding ??= "utf8"
28
+
29
+ let buffer = Buffer.from("")
30
+ let entry = []
31
+
32
+ readable.on('data', (chunk, encoding) => {
33
+ if (typeof chunk == 'string') {
34
+ buffer = Buffer.concat([buffer, Buffer.from(chunk, encoding)])
35
+ } else {
36
+ buffer = Buffer.concat([buffer, chunk])
37
+ }
38
+
39
+ let delimiterIndex = buffer.indexOf(delimiter)
40
+ while (delimiterIndex > -1) {
41
+ const slice = buffer.slice(0, delimiterIndex)
42
+ entry.push(slice)
43
+ if (entry.length == entryLength) {
44
+ this.emit("entry", entry, encoding)
45
+ entry = []
46
+ }
47
+ buffer = buffer.slice(delimiterIndex + 1)
48
+ delimiterIndex = buffer.indexOf(delimiter)
49
+ }
50
+ })
51
+
52
+ readable.on('end', () => {
53
+ if (buffer.length > 0) {
54
+ entry.push(buffer)
55
+ }
56
+ if (entry.length == entryLength) {
57
+ this.emit("entry", entry, encoding)
58
+ this.emit("end")
59
+ entry = []
60
+ } else if (entry.length > 0) {
61
+ const error = new Error("last entry")
62
+ error.entry = entry
63
+ this.emit("error", error)
64
+ } else {
65
+ this.emit("end")
66
+ }
67
+ })
68
+ }
69
+ }
70
+
71
+ module.exports = BinaryEntriesStream
@@ -0,0 +1,50 @@
1
+ const EventEmitter = require("events")
2
+
3
+ /**
4
+ * @name BinaryStream
5
+ *
6
+ * @docs
7
+ * ```coffeescript [specscript]
8
+ * module stream 'https://nodejs.org/docs/latest-v26.x/api/stream.html'
9
+ * module events 'https://nodejs.org/docs/latest-v26.x/api/events.html'
10
+ *
11
+ * BinaryStream(readable stream.Readable, delimiter string) -> binaryStream events.EventEmitter
12
+ *
13
+ * textStream.on("data", chunk Buffer -> ())
14
+ *
15
+ * textStream.on("end", () -> ())
16
+ * ```
17
+ */
18
+ class BinaryStream extends EventEmitter {
19
+ constructor(readable, delimiter) {
20
+ super()
21
+
22
+ delimiter ??= ""
23
+
24
+ let buffer = Buffer.from("")
25
+ readable.on('data', (chunk, encoding) => {
26
+ if (typeof chunk == 'string') {
27
+ buffer = Buffer.concat([buffer, Buffer.from(chunk, encoding)])
28
+ } else {
29
+ buffer = Buffer.concat([buffer, chunk])
30
+ }
31
+
32
+ let delimiterIndex = buffer.indexOf(delimiter)
33
+ while (delimiterIndex > -1) {
34
+ const slice = buffer.slice(0, delimiterIndex)
35
+ this.emit("data", slice)
36
+ buffer = buffer.slice(delimiterIndex + 1)
37
+ delimiterIndex = buffer.indexOf(delimiter)
38
+ }
39
+ })
40
+
41
+ readable.on('end', () => {
42
+ if (buffer.length > 0) {
43
+ this.emit("data", buffer)
44
+ }
45
+ this.emit("end")
46
+ })
47
+ }
48
+ }
49
+
50
+ module.exports = BinaryStream
@@ -0,0 +1,288 @@
1
+ require("rubico/global")
2
+ const DiskHashTable = require("presidium/DiskHashTable")
3
+ const DiskSortedHashTable = require("presidium/DiskSortedHashTable")
4
+ const Readable = require("presidium/Readable")
5
+
6
+ /**
7
+ * @name DatabaseHandler
8
+ *
9
+ * @docs
10
+ * ```specscript
11
+ * module DiskHashTable "https://presidium.services/docs/DiskHashTable"
12
+ * module HTTP "https://presidium.services/docs/HTTP"
13
+ *
14
+ * DatabaseHandler(options {
15
+ * databasePath: string,
16
+ * manifestTableName: string,
17
+ * manifestTableInitialLength: number,
18
+ * manifestTableItemSize: number,
19
+ * defaultTableInitialLength: number,
20
+ * defaultTableItemSize: number,
21
+ * manifestHt: DiskHashTable,
22
+ * hts: Map<tableName string, DiskHashTable>,
23
+ * }) -> databaseHandler HTTP.AsyncHandler
24
+ * ```
25
+ */
26
+ function DatabaseHandler(options) {
27
+ const {
28
+ databasePath,
29
+ manifestTableName,
30
+ manifestTableInitialLength,
31
+ manifestTableItemSize,
32
+ defaultTableInitialLength,
33
+ defaultTableItemSize,
34
+ manifestHt,
35
+ hts,
36
+ } = options
37
+
38
+ /**
39
+ * @name getHt
40
+ *
41
+ * @docs
42
+ * ```specscript
43
+ * module HTTP "https://presidium.services/docs/HTTP"
44
+ *
45
+ * getHt(tableName string, request HTTP.Request) -> ht DiskHashTable|DiskSortedHashTable
46
+ * ```
47
+ */
48
+ async function getHt(tableName, request, manifestHt) {
49
+ const htConfigString = await manifestHt.get(tableName)
50
+ let ht
51
+ if (htConfigString) {
52
+ const config = JSON.parse(htConfigString)
53
+ if (request.headers["client-name"] == "DiskHashTableClient") {
54
+ ht = new DiskHashTable(config)
55
+ await ht.init()
56
+ hts.set(tableName, ht)
57
+ } else if (request.headers["client-name"] == "DiskSortedHashTableClient") {
58
+ ht = new DiskSortedHashTable(config)
59
+ await ht.init()
60
+ hts.set(tableName, ht)
61
+ } else {
62
+ const error = new Error("Forbidden: Client-Name")
63
+ error.code = 403
64
+ throw error
65
+ }
66
+ } else {
67
+ if (request.headers["client-name"] == "DiskHashTableClient") {
68
+ const config = {
69
+ storagePath: `${databasePath}/${tableName}/storage`,
70
+ headerPath: `${databasePath}/${tableName}/header`,
71
+ initialLength: defaultTableInitialLength,
72
+ itemSize: defaultTableItemSize,
73
+ }
74
+ ht = new DiskHashTable(config)
75
+ await manifestHt.set(tableName, JSON.stringify(config))
76
+ await ht.init()
77
+ hts.set(tableName, ht)
78
+ } else if (request.headers["client-name"] == "DiskSortedHashTableClient") {
79
+ const config = {
80
+ storagePath: `${databasePath}/${tableName}/storage`,
81
+ headerPath: `${databasePath}/${tableName}/header`,
82
+ initialLength: defaultTableInitialLength,
83
+ itemSize: defaultTableItemSize,
84
+ sortValueType: request.headers["sort-value-type"],
85
+ degree: Number(request.headers["degree"]),
86
+ }
87
+ ht = new DiskSortedHashTable(config)
88
+ await manifestHt.set(tableName, JSON.stringify(config))
89
+ await ht.init()
90
+ hts.set(tableName, ht)
91
+ } else {
92
+ const error = new Error("Forbidden: Client-Name")
93
+ error.code = 403
94
+ throw error
95
+ }
96
+ }
97
+ return ht
98
+ }
99
+
100
+ return async function databaseHandler(request, response) {
101
+ const { pathname } = new URL(`http://*${request.url}`)
102
+ request.path = decodeURIComponent(
103
+ pathname == "/" ? pathname
104
+ : pathname.endsWith("/") ? pathname.slice(0, -1)
105
+ : pathname
106
+ )
107
+ request.pathSegments = request.path.slice(1).split("/")
108
+ request.pathDepth = request.pathSegments.length
109
+
110
+ if (request.pathDepth == 2) {
111
+ const tableName = request.pathSegments[0]
112
+ const fname = request.pathSegments[1]
113
+ const ht = hts.get(tableName)
114
+ ?? await getHt(tableName, request, manifestHt)
115
+
116
+ await switchCase(
117
+ fname,
118
+
119
+ eq("clear"), async () => {
120
+ await ht.clear()
121
+ response.writeHead(200, {
122
+ "Content-Type": "text/plain",
123
+ })
124
+ response.end("OK")
125
+ },
126
+
127
+ eq("set"), async () => {
128
+ const { key, value, sortValue } = await Readable.JSON(request)
129
+ await ht.set(key, value, sortValue)
130
+ response.writeHead(200, {
131
+ "Content-Type": "text/plain",
132
+ })
133
+ response.end("OK")
134
+ },
135
+
136
+ eq("get"), async () => {
137
+ const { key } = await Readable.JSON(request)
138
+ const value = await ht.get(key)
139
+ if (value === undefined) {
140
+ response.writeHead(499, {
141
+ "Content-Type": "text/plain",
142
+ })
143
+ response.end("Resource Not Found")
144
+ } else {
145
+ response.writeHead(200, {
146
+ "Content-Type": "text/plain",
147
+ })
148
+ response.end(value)
149
+ }
150
+ },
151
+
152
+ eq("getBinary"), async () => {
153
+ const { key } = await Readable.JSON(request)
154
+ const value = await ht.getBinary(key)
155
+ response.writeHead(200, {
156
+ "Content-Type": "application/octet-stream",
157
+ })
158
+ response.end(value)
159
+ },
160
+
161
+ eq("delete"), async () => {
162
+ const { key } = await Readable.JSON(request)
163
+ const didDelete = await ht.delete(key)
164
+ response.writeHead(200, {
165
+ "Content-Type": "text/plain",
166
+ })
167
+ response.end(String(didDelete))
168
+ },
169
+
170
+ eq("count"), async () => {
171
+ const count = ht.count()
172
+ response.writeHead(200, {
173
+ "Content-Type": "text/plain",
174
+ })
175
+ response.end(String(count))
176
+ },
177
+
178
+ eq("iterator"), async () => {
179
+ const options = await Readable.JSON(request)
180
+ response.writeHead(200, {
181
+ "Content-Type": "application/octet-stream",
182
+ "Content-Encoding": "utf8",
183
+ })
184
+ const count = ht.count()
185
+ for await (const value of ht.iterator(options)) {
186
+ response.write(value)
187
+ response.write("")
188
+ }
189
+ response.end()
190
+ },
191
+
192
+ eq("entriesIterator"), async () => {
193
+ const options = await Readable.JSON(request)
194
+ response.writeHead(200, {
195
+ "Content-Type": "application/octet-stream",
196
+ "Content-Encoding": "utf8",
197
+ })
198
+ const count = ht.count()
199
+ for await (const [key, value] of ht.entriesIterator(options)) {
200
+ response.write(key)
201
+ response.write("")
202
+ response.write(value)
203
+ response.write("")
204
+ }
205
+ response.end()
206
+ },
207
+
208
+ eq("forwardIterator"), async () => {
209
+ const options = await Readable.JSON(request)
210
+ response.writeHead(200, {
211
+ "Content-Type": "application/octet-stream",
212
+ "Content-Encoding": "utf8",
213
+ })
214
+ const count = ht.count()
215
+ for await (const value of ht.forwardIterator(options)) {
216
+ response.write(value)
217
+ response.write("")
218
+ }
219
+ response.end()
220
+ },
221
+
222
+ eq("reverseIterator"), async () => {
223
+ const options = await Readable.JSON(request)
224
+ response.writeHead(200, {
225
+ "Content-Type": "application/octet-stream",
226
+ "Content-Encoding": "utf8",
227
+ })
228
+ const count = ht.count()
229
+ for await (const value of ht.reverseIterator(options)) {
230
+ response.write(value)
231
+ response.write("")
232
+ }
233
+ response.end()
234
+ },
235
+
236
+ eq("forwardEntriesIterator"), async () => {
237
+ const options = await Readable.JSON(request)
238
+ response.writeHead(200, {
239
+ "Content-Type": "application/octet-stream",
240
+ "Content-Encoding": "utf8",
241
+ })
242
+ const count = ht.count()
243
+ for await (const [key, value, sortValue] of ht.forwardEntriesIterator(options)) {
244
+ response.write(key)
245
+ response.write("")
246
+ response.write(value)
247
+ response.write("")
248
+ response.write(String(sortValue))
249
+ response.write("")
250
+ }
251
+ response.end()
252
+ },
253
+
254
+ eq("reverseEntriesIterator"), async () => {
255
+ const options = await Readable.JSON(request)
256
+ response.writeHead(200, {
257
+ "Content-Type": "application/octet-stream",
258
+ "Content-Encoding": "utf8",
259
+ })
260
+ const count = ht.count()
261
+ for await (const [key, value, sortValue] of ht.reverseEntriesIterator(options)) {
262
+ response.write(key)
263
+ response.write("")
264
+ response.write(value)
265
+ response.write("")
266
+ response.write(String(sortValue))
267
+ response.write("")
268
+ }
269
+ response.end()
270
+ },
271
+
272
+ async () => {
273
+ const error = new Error("Forbidden: Hash Table Method")
274
+ error.code = "403"
275
+ throw error
276
+ },
277
+ )
278
+
279
+ } else {
280
+ response.writeHead(404, {
281
+ "Content-Type": "text/plain",
282
+ })
283
+ response.end("Not Found")
284
+ }
285
+ }
286
+ }
287
+
288
+ module.exports = DatabaseHandler
@@ -0,0 +1,97 @@
1
+ const HTTP = require("presidium/HTTP")
2
+ const DiskHashTable = require("presidium/DiskHashTable")
3
+ const DatabaseHandler = require("./DatabaseHandler")
4
+
5
+ /**
6
+ * @name DatabaseServer
7
+ *
8
+ * @docs
9
+ * ```specscript
10
+ * new DatabaseServer(options {
11
+ * databasePath: string,
12
+ * manifestTableName: string,
13
+ * manifestTableInitialLength: number,
14
+ * manifestTableItemSize: number,
15
+ * defaultTableInitialLength: number,
16
+ * defaultTableItemSize: number,
17
+ * defaultDiskSortedHashTableDegree: number,
18
+ * }) -> server DatabaseServer
19
+ * ```
20
+ */
21
+ class DatabaseServer {
22
+ constructor(options) {
23
+ this.databasePath = options.databasePath
24
+ this.manifestTableName = options.manifestTableName ?? "Manifest"
25
+ this.manifestTableInitialLength = options.manifestTableInitialLength ?? 1048576
26
+ this.manifestTableItemSize = options.manifestTableItemSize ?? 1024
27
+ this.defaultTableInitialLength = options.defaultTableInitialLength ?? 1048576
28
+ this.defaultTableItemSize = options.defaultTableItemSize ?? 1024
29
+ this.defaultDiskSortedHashTableDegree = options.defaultDiskSortedHashTableDegree ?? 2
30
+ this.server = null
31
+ this.hts = new Map()
32
+ }
33
+
34
+ /**
35
+ * @name init
36
+ *
37
+ * @docs
38
+ * ```specscript
39
+ * init() -> Promise<>
40
+ * ```
41
+ */
42
+ async init() {
43
+ const manifestHt = new DiskHashTable({
44
+ storagePath: `${this.databasePath}/${this.manifestTableName}/storage`,
45
+ headerPath: `${this.databasePath}/${this.manifestTableName}/header`,
46
+ initialLength: this.manifestTableInitialLength,
47
+ itemSize: this.manifestTableItemSize,
48
+ })
49
+ await manifestHt.init()
50
+ this.manifestHt = manifestHt
51
+
52
+ const databaseHandler = DatabaseHandler({
53
+ databasePath: this.databasePath,
54
+ manifestTableName: this.manifestTableName,
55
+ manifestTableInitialLength: this.manifestTableInitialLength,
56
+ manifestTableItemSize: this.manifestTableItemSize,
57
+ defaultTableInitialLength: this.defaultTableInitialLength,
58
+ defaultTableItemSize: this.defaultTableItemSize,
59
+ defaultDiskSortedHashTableDegree: this.defaultDiskSortedHashTableDegree,
60
+ manifestHt: this.manifestHt,
61
+ hts: this.hts,
62
+ })
63
+
64
+ this.server = HTTP.Server(databaseHandler)
65
+ }
66
+
67
+ /**
68
+ * @name listen
69
+ *
70
+ * @docs
71
+ * ```specscript
72
+ * listen(port number) -> ()
73
+ * listen(port number, callback function) -> ()
74
+ * ```
75
+ */
76
+ listen(port, callback) {
77
+ this.server.listen(port, callback)
78
+ }
79
+
80
+ /**
81
+ * @name close
82
+ *
83
+ * @docs
84
+ * ```specscript
85
+ * close() -> ()
86
+ * ```
87
+ */
88
+ close() {
89
+ this.manifestHt.close()
90
+ for (const ht of this.hts.values()) {
91
+ ht.close()
92
+ }
93
+ this.server.close()
94
+ }
95
+ }
96
+
97
+ module.exports = DatabaseServer
@@ -0,0 +1,220 @@
1
+ require("rubico/global")
2
+ const HTTP = require("presidium/HTTP")
3
+ const Readable = require("presidium/Readable")
4
+ const handleServerResponse = require("./handleServerResponse")
5
+ const buffersToEntry = require("./buffersToEntry")
6
+
7
+ /**
8
+ * @name DiskHashTableClient
9
+ *
10
+ * @docs
11
+ * ```specscript
12
+ * new DiskHashTableClient(options {
13
+ * serverUrl: string,
14
+ * tableName: string,
15
+ * delimiter: string,
16
+ * }) -> DiskHashTableClient
17
+ * ```
18
+ */
19
+ class DiskHashTableClient {
20
+ constructor(options) {
21
+ this.serverUrl = options.serverUrl
22
+ this.tableName = options.tableName
23
+ this.delimiter = options.delimiter ?? ""
24
+ this.http = new HTTP(this.serverUrl)
25
+ }
26
+
27
+ /**
28
+ * @name clear
29
+ *
30
+ * @docs
31
+ * ```specscript
32
+ * module DiskHashTable "https://presidium.services/docs/DiskHashTable"
33
+ *
34
+ * clear DiskHashTable.clear
35
+ * ```
36
+ */
37
+ async clear() {
38
+ const response = await this.http.POST(`/${this.tableName}/clear`, {
39
+ headers: {
40
+ "Client-Name": "DiskHashTableClient",
41
+ },
42
+ })
43
+ return handleServerResponse(response)
44
+ }
45
+
46
+ /**
47
+ * @name set
48
+ *
49
+ * @docs
50
+ * ```specscript
51
+ * module DiskHashTable "https://presidium.services/docs/DiskHashTable"
52
+ *
53
+ * set DiskHashTable.set
54
+ * ```
55
+ */
56
+ async set(key, value) {
57
+ const response = await this.http.POST(`/${this.tableName}/set`, {
58
+ headers: {
59
+ "Content-Type": "application/json",
60
+ "Client-Name": "DiskHashTableClient",
61
+ },
62
+ body: JSON.stringify({ key, value }),
63
+ })
64
+ await handleServerResponse(response)
65
+ }
66
+
67
+ /**
68
+ * @name get
69
+ *
70
+ * @docs
71
+ * ```specscript
72
+ * module DiskHashTable "https://presidium.services/docs/DiskHashTable"
73
+ *
74
+ * get DiskHashTable.get
75
+ * ```
76
+ */
77
+ async get(key) {
78
+ const response = await this.http.POST(`/${this.tableName}/get`, {
79
+ headers: {
80
+ "Content-Type": "application/json",
81
+ "Client-Name": "DiskHashTableClient",
82
+ },
83
+ body: JSON.stringify({ key }),
84
+ })
85
+ if (response.status == 499) {
86
+ return undefined
87
+ }
88
+ return handleServerResponse(response)
89
+ }
90
+
91
+ /**
92
+ * @name getBinary
93
+ *
94
+ * @docs
95
+ * ```specscript
96
+ * module DiskHashTable "https://presidium.services/docs/DiskHashTable"
97
+ *
98
+ * getBinary DiskHashTable.getBinary
99
+ * ```
100
+ */
101
+ async getBinary(key) {
102
+ const response = await this.http.POST(`/${this.tableName}/getBinary`, {
103
+ headers: {
104
+ "Content-Type": "application/json",
105
+ "Client-Name": "DiskHashTableClient",
106
+ },
107
+ body: JSON.stringify({ key }),
108
+ })
109
+ return handleServerResponse(response)
110
+ }
111
+
112
+ /**
113
+ * @name delete
114
+ *
115
+ * @docs
116
+ * ```specscript
117
+ * module DiskHashTable "https://presidium.services/docs/DiskHashTable"
118
+ *
119
+ * delete DiskHashTable.delete
120
+ * ```
121
+ */
122
+ async delete(key) {
123
+ const response = await this.http.POST(`/${this.tableName}/delete`, {
124
+ headers: {
125
+ "Content-Type": "application/json",
126
+ "Client-Name": "DiskHashTableClient",
127
+ },
128
+ body: JSON.stringify({ key }),
129
+ })
130
+ const didDeleteText = await handleServerResponse(response)
131
+ return didDeleteText == "true"
132
+ }
133
+
134
+ /**
135
+ * @name count
136
+ *
137
+ * @docs
138
+ * ```specscript
139
+ * count() -> Promise<number>
140
+ * ```
141
+ */
142
+ async count() {
143
+ const response = await this.http.POST(`/${this.tableName}/count`, {
144
+ headers: {
145
+ "Client-Name": "DiskHashTableClient",
146
+ },
147
+ })
148
+ const scount = await handleServerResponse(response)
149
+ return Number(scount)
150
+ }
151
+
152
+ /**
153
+ * @name iterator
154
+ *
155
+ * @docs
156
+ * ```specscript
157
+ * iterator(options {
158
+ * valueType: "string"|"binary",
159
+ * }) -> values AsyncIterator<string|Buffer>
160
+ * ```
161
+ */
162
+ async iterator(options = {}) {
163
+ const { valueType } = options
164
+ const response = await this.http.POST(`/${this.tableName}/iterator`, {
165
+ headers: {
166
+ "Content-Type": "application/json",
167
+ "Client-Name": "DiskHashTableClient",
168
+ },
169
+ body: JSON.stringify(options),
170
+ })
171
+ if (valueType == "binary") {
172
+ return handleServerResponse(response, {
173
+ binaryStream: true,
174
+ delimiter: this.delimiter,
175
+ })
176
+ }
177
+ return handleServerResponse(response, {
178
+ textStream: true,
179
+ delimiter: this.delimiter,
180
+ })
181
+ }
182
+
183
+ /**
184
+ * @name entriesIterator
185
+ *
186
+ * @docs
187
+ * ```specscript
188
+ * entriesIterator(options {
189
+ * valueType: 'string'|'binary',
190
+ * }) -> entries AsyncIterator<[key string, value string|Buffer]>
191
+ * ```
192
+ */
193
+ async entriesIterator(options = {}) {
194
+ const { valueType } = options
195
+ const response = await this.http.POST(`/${this.tableName}/entriesIterator`, {
196
+ headers: {
197
+ "Content-Type": "application/json",
198
+ "Client-Name": "DiskHashTableClient",
199
+ },
200
+ body: JSON.stringify({ valueType }),
201
+ })
202
+
203
+ if (valueType == "binary") {
204
+ const iter = await handleServerResponse(response, {
205
+ binaryEntriesStream: true,
206
+ delimiter: this.delimiter,
207
+ entryLength: 2,
208
+ })
209
+ return map(iter, curry.arity(4, buffersToEntry, __, 2, "utf8", valueType))
210
+ }
211
+ const iter = await handleServerResponse(response, {
212
+ textEntriesStream: true,
213
+ delimiter: this.delimiter,
214
+ entryLength: 2,
215
+ })
216
+ return map(iter, curry.arity(4, buffersToEntry, __, 2, "utf8", valueType))
217
+ }
218
+ }
219
+
220
+ module.exports = DiskHashTableClient
@@ -0,0 +1,317 @@
1
+ require("rubico/global")
2
+ const HTTP = require("presidium/HTTP")
3
+ const handleServerResponse = require("./handleServerResponse")
4
+ const buffersToEntry = require("./buffersToEntry")
5
+
6
+ /**
7
+ * @name DiskSortedHashTableClient
8
+ *
9
+ * @docs
10
+ * ```specscript
11
+ * new DiskSortedHashTableClient(options {
12
+ * serverUrl: string,
13
+ * tableName: string,
14
+ * sortValueType: "string"|"number",
15
+ * degree: number,
16
+ * delimiter: string,
17
+ * }) -> DiskSortedHashTableClient
18
+ * ```
19
+ */
20
+ class DiskSortedHashTableClient {
21
+ constructor(options) {
22
+ this.serverUrl = options.serverUrl
23
+ this.tableName = options.tableName
24
+ this.sortValueType = options.sortValueType
25
+ this.degree = options.degree ?? 2
26
+ this.delimiter = options.delimiter ?? ""
27
+ this.http = new HTTP(this.serverUrl)
28
+ }
29
+
30
+ /**
31
+ * @name clear
32
+ *
33
+ * @docs
34
+ * ```specscript
35
+ * module DiskSortedHashTable "https://presidium.services/docs/DiskSortedHashTable"
36
+ *
37
+ * clear DiskSortedHashTable.clear
38
+ * ```
39
+ */
40
+ async clear() {
41
+ const response = await this.http.POST(`/${this.tableName}/clear`, {
42
+ headers: {
43
+ "Client-Name": "DiskSortedHashTableClient",
44
+ "Sort-Value-Type": this.sortValueType,
45
+ "Degree": Number(this.degree),
46
+ },
47
+ })
48
+ return handleServerResponse(response)
49
+ }
50
+
51
+ /**
52
+ * @name set
53
+ *
54
+ * @docs
55
+ * ```specscript
56
+ * module DiskSortedHashTable "https://presidium.services/docs/DiskSortedHashTable"
57
+ *
58
+ * set DiskSortedHashTable.set
59
+ * ```
60
+ */
61
+ async set(key, value, sortValue) {
62
+ const response = await this.http.POST(`/${this.tableName}/set`, {
63
+ headers: {
64
+ "Content-Type": "application/json",
65
+ "Client-Name": "DiskSortedHashTableClient",
66
+ "Sort-Value-Type": this.sortValueType,
67
+ "Degree": Number(this.degree),
68
+ },
69
+ body: JSON.stringify({ key, value, sortValue }),
70
+ })
71
+ await handleServerResponse(response)
72
+ }
73
+
74
+ /**
75
+ * @name get
76
+ *
77
+ * @docs
78
+ * ```specscript
79
+ * module DiskSortedHashTable "https://presidium.services/docs/DiskSortedHashTable"
80
+ *
81
+ * get DiskSortedHashTable.get
82
+ * ```
83
+ */
84
+ async get(key) {
85
+ const response = await this.http.POST(`/${this.tableName}/get`, {
86
+ headers: {
87
+ "Content-Type": "application/json",
88
+ "Client-Name": "DiskSortedHashTableClient",
89
+ "Sort-Value-Type": this.sortValueType,
90
+ "Degree": Number(this.degree),
91
+ },
92
+ body: JSON.stringify({ key }),
93
+ })
94
+ if (response.status == 499) {
95
+ return undefined
96
+ }
97
+ return handleServerResponse(response)
98
+ }
99
+
100
+ /**
101
+ * @name getBinary
102
+ *
103
+ * @docs
104
+ * ```specscript
105
+ * module DiskSortedHashTable "https://presidium.services/docs/DiskSortedHashTable"
106
+ *
107
+ * getBinary DiskSortedHashTable.getBinary
108
+ * ```
109
+ */
110
+ async getBinary(key) {
111
+ const response = await this.http.POST(`/${this.tableName}/getBinary`, {
112
+ headers: {
113
+ "Content-Type": "application/json",
114
+ "Client-Name": "DiskSortedHashTableClient",
115
+ "Sort-Value-Type": this.sortValueType,
116
+ "Degree": Number(this.degree),
117
+ },
118
+ body: JSON.stringify({ key }),
119
+ })
120
+ return handleServerResponse(response)
121
+ }
122
+
123
+ /**
124
+ * @name forwardIterator
125
+ *
126
+ * @docs
127
+ * ```specscript
128
+ * forwardIterator(options {
129
+ * exclusiveStartKey: string,
130
+ * startingSortValue: string|number,
131
+ * endingSortValue: string|number,
132
+ * valueType: 'string'|'binary',
133
+ * }) -> values AsyncIterator<string|Buffer>
134
+ * ```
135
+ */
136
+ async forwardIterator(options = {}) {
137
+ const { valueType } = options
138
+ const response = await this.http.POST(`/${this.tableName}/forwardIterator`, {
139
+ headers: {
140
+ "Content-Type": "application/json",
141
+ "Client-Name": "DiskSortedHashTableClient",
142
+ "Sort-Value-Type": this.sortValueType,
143
+ "Degree": Number(this.degree),
144
+ },
145
+ body: JSON.stringify(options),
146
+ })
147
+ if (valueType == "binary") {
148
+ return handleServerResponse(response, {
149
+ binaryStream: true,
150
+ delimiter: this.delimiter,
151
+ })
152
+ }
153
+ return handleServerResponse(response, {
154
+ textStream: true,
155
+ delimiter: this.delimiter,
156
+ })
157
+ }
158
+
159
+ /**
160
+ * @name reverseIterator
161
+ *
162
+ * @docs
163
+ * ```specscript
164
+ * reverseIterator(options {
165
+ * exclusiveStartKey: string,
166
+ * startingSortValue: string|number,
167
+ * endingSortValue: string|number,
168
+ * valueType: 'string'|'binary',
169
+ * }) -> values AsyncIterator<string|Buffer>
170
+ * ```
171
+ */
172
+ async reverseIterator(options = {}) {
173
+ const { valueType } = options
174
+ const response = await this.http.POST(`/${this.tableName}/reverseIterator`, {
175
+ headers: {
176
+ "Content-Type": "application/json",
177
+ "Client-Name": "DiskSortedHashTableClient",
178
+ "Sort-Value-Type": this.sortValueType,
179
+ "Degree": Number(this.degree),
180
+ },
181
+ body: JSON.stringify(options),
182
+ })
183
+ if (valueType == "binary") {
184
+ return handleServerResponse(response, {
185
+ binaryStream: true,
186
+ delimiter: this.delimiter,
187
+ })
188
+ }
189
+ return handleServerResponse(response, {
190
+ textStream: true,
191
+ delimiter: this.delimiter,
192
+ })
193
+ }
194
+
195
+ /**
196
+ * @name forwardEntriesIterator
197
+ *
198
+ * @docs
199
+ * ```specscript
200
+ * forwardEntriesIterator(options {
201
+ * exclusiveStartKey: string,
202
+ * startingSortValue: string|number,
203
+ * endingSortValue: string|number,
204
+ * valueType: 'string'|'binary',
205
+ * }) -> values AsyncIterator<string|Buffer>
206
+ * ```
207
+ */
208
+ async forwardEntriesIterator(options = {}) {
209
+ const { valueType } = options
210
+ const response = await this.http.POST(`/${this.tableName}/forwardEntriesIterator`, {
211
+ headers: {
212
+ "Content-Type": "application/json",
213
+ "Client-Name": "DiskSortedHashTableClient",
214
+ "Sort-Value-Type": this.sortValueType,
215
+ "Degree": Number(this.degree),
216
+ },
217
+ body: JSON.stringify(options),
218
+ })
219
+ if (valueType == "binary") {
220
+ const iter = await handleServerResponse(response, {
221
+ binaryEntriesStream: true,
222
+ delimiter: this.delimiter,
223
+ entryLength: 3,
224
+ })
225
+ return map(iter, curry(buffersToEntry, __, 3, "utf8", valueType, this.sortValueType))
226
+ }
227
+ const iter = await handleServerResponse(response, {
228
+ textEntriesStream: true,
229
+ delimiter: this.delimiter,
230
+ entryLength: 3,
231
+ })
232
+ return map(iter, curry(buffersToEntry, __, 3, "utf8", valueType, this.sortValueType))
233
+ }
234
+
235
+ /**
236
+ * @name reverseEntriesIterator
237
+ *
238
+ * @docs
239
+ * ```specscript
240
+ * module DiskSortedHashTable "https://presidium.services/docs/DiskSortedHashTable"
241
+ *
242
+ * reverseEntriesIterator DiskSortedHashTable.reverseEntriesIterator
243
+ * ```
244
+ */
245
+ async reverseEntriesIterator(options = {}) {
246
+ const { valueType } = options
247
+ const response = await this.http.POST(`/${this.tableName}/reverseEntriesIterator`, {
248
+ headers: {
249
+ "Content-Type": "application/json",
250
+ "Client-Name": "DiskSortedHashTableClient",
251
+ "Sort-Value-Type": this.sortValueType,
252
+ "Degree": Number(this.degree),
253
+ },
254
+ body: JSON.stringify(options),
255
+ })
256
+
257
+ if (valueType == "binary") {
258
+ const iter = await handleServerResponse(response, {
259
+ binaryEntriesStream: true,
260
+ delimiter: this.delimiter,
261
+ entryLength: 3,
262
+ })
263
+ return map(iter, curry(buffersToEntry, __, 3, "utf8", valueType, this.sortValueType))
264
+ }
265
+ const iter = await handleServerResponse(response, {
266
+ textEntriesStream: true,
267
+ delimiter: this.delimiter,
268
+ entryLength: 3,
269
+ })
270
+ return map(iter, curry(buffersToEntry, __, 3, "utf8", valueType, this.sortValueType))
271
+ }
272
+
273
+ /**
274
+ * @name delete
275
+ *
276
+ * @docs
277
+ * ```specscript
278
+ * module DiskSortedHashTable "https://presidium.services/docs/DiskSortedHashTable"
279
+ *
280
+ * delete DiskSortedHashTable.delete
281
+ * ```
282
+ */
283
+ async delete(key) {
284
+ const response = await this.http.POST(`/${this.tableName}/delete`, {
285
+ headers: {
286
+ "Content-Type": "application/json",
287
+ "Client-Name": "DiskSortedHashTableClient",
288
+ "Sort-Value-Type": this.sortValueType,
289
+ "Degree": Number(this.degree),
290
+ },
291
+ body: JSON.stringify({ key }),
292
+ })
293
+ const didDeleteText = await handleServerResponse(response)
294
+ return didDeleteText == "true"
295
+ }
296
+
297
+ /**
298
+ * @name count
299
+ *
300
+ * @docs
301
+ * ```specscript
302
+ * count() -> Promise<number>
303
+ * ```
304
+ */
305
+ async count() {
306
+ const response = await this.http.POST(`/${this.tableName}/count`, {
307
+ headers: {
308
+ "Client-Name": "DiskSortedHashTableClient",
309
+ "Sort-Value-Type": this.sortValueType,
310
+ "Degree": Number(this.degree),
311
+ },
312
+ })
313
+ return Number(await handleServerResponse(response))
314
+ }
315
+ }
316
+
317
+ module.exports = DiskSortedHashTableClient
@@ -0,0 +1,69 @@
1
+ const EventEmitter = require("events")
2
+
3
+ /**
4
+ * @name TextEntriesStream
5
+ *
6
+ * @docs
7
+ * ```coffeescript [specscript]
8
+ * module EventEmitter "./specscript/EventEmitter.ss"
9
+ *
10
+ * TextEntriesStream(
11
+ * readable stream.Readable,
12
+ * entryLength number,
13
+ * delimiter string
14
+ * ) -> textEntriesStream EventEmitter
15
+ *
16
+ * textEntriesStream.on("entry", (entry Array<string>, keyEncoding string) -> ())
17
+ *
18
+ * textEntriesStream.on("end", () -> ())
19
+ * ```
20
+ */
21
+ class TextEntriesStream extends EventEmitter {
22
+ constructor(readable, entryLength, delimiter, encoding) {
23
+ super()
24
+
25
+ delimiter ??= ""
26
+ encoding ??= "utf8"
27
+
28
+ let s = ""
29
+ let entry = []
30
+ readable.on('data', (chunk, encoding) => {
31
+ if (typeof chunk == 'string') {
32
+ s += Buffer.from(chunk, encoding).toString(encoding)
33
+ } else {
34
+ s += chunk.toString(encoding)
35
+ }
36
+
37
+ let delimiterIndex = s.indexOf(delimiter)
38
+ while (delimiterIndex > -1) {
39
+ const text = s.slice(0, delimiterIndex)
40
+ entry.push(text)
41
+ if (entry.length == entryLength) {
42
+ this.emit("entry", entry, encoding)
43
+ entry = []
44
+ }
45
+ s = s.slice(delimiterIndex + 1)
46
+ delimiterIndex = s.indexOf(delimiter)
47
+ }
48
+ })
49
+
50
+ readable.on("end", () => {
51
+ if (s.length > 0) {
52
+ entry.push(s)
53
+ }
54
+ if (entry.length == entryLength) {
55
+ this.emit("entry", entry, encoding)
56
+ this.emit("end")
57
+ entry = []
58
+ } else if (entry.length > 0) {
59
+ const error = new Error("last entry")
60
+ error.entry = entry
61
+ this.emit("error", error)
62
+ } else {
63
+ this.emit("end")
64
+ }
65
+ })
66
+ }
67
+ }
68
+
69
+ module.exports = TextEntriesStream
package/TextStream.js ADDED
@@ -0,0 +1,51 @@
1
+ const EventEmitter = require("events")
2
+
3
+ /**
4
+ * @name TextStream
5
+ *
6
+ * @docs
7
+ * ```coffeescript [specscript]
8
+ * module stream 'https://nodejs.org/docs/latest-v26.x/api/stream.html'
9
+ * module events 'https://nodejs.org/docs/latest-v26.x/api/events.html'
10
+ *
11
+ * TextStream(readable stream.Readable, delimiter string) -> textStream events.EventEmitter
12
+ *
13
+ * textStream.on("data", (text string, encoding string) -> ())
14
+ *
15
+ * textStream.on("end", () -> ())
16
+ * ```
17
+ */
18
+ class TextStream extends EventEmitter {
19
+ constructor(readable, delimiter, encoding) {
20
+ super()
21
+
22
+ delimiter ??= ""
23
+ encoding ??= "utf8"
24
+
25
+ let s = ""
26
+ readable.on('data', (chunk, encoding) => {
27
+ if (typeof chunk == 'string') {
28
+ s += Buffer.from(chunk, encoding).toString(encoding)
29
+ } else {
30
+ s += chunk.toString(encoding)
31
+ }
32
+
33
+ let delimiterIndex = s.indexOf(delimiter)
34
+ while (delimiterIndex > -1) {
35
+ const text = s.slice(0, delimiterIndex)
36
+ this.emit("data", text, encoding)
37
+ s = s.slice(delimiterIndex + 1)
38
+ delimiterIndex = s.indexOf(delimiter)
39
+ }
40
+ })
41
+
42
+ readable.on('end', () => {
43
+ if (s.length > 0) {
44
+ this.emit("data", s, encoding)
45
+ }
46
+ this.emit("end")
47
+ })
48
+ }
49
+ }
50
+
51
+ module.exports = TextStream
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @name buffersToEntry
3
+ *
4
+ * @docs
5
+ * ```specscript
6
+ * buffersToEntry(
7
+ * buffers Array<Buffer>,
8
+ * entryLength number,
9
+ * encoding string,
10
+ * valueType string,
11
+ * ) -> Array<string, Buffer>
12
+ *
13
+ * buffersToEntry(
14
+ * buffers Array<Buffer>,
15
+ * entryLength number,
16
+ * encoding string,
17
+ * valueType string,
18
+ * sortValueType string
19
+ * ) -> Array<string, Buffer>
20
+ * ```
21
+ */
22
+ function buffersToEntry(buffers, entryLength, encoding, valueType, sortValueType) {
23
+ if (entryLength == 3) {
24
+ const key = buffers[0].toString(encoding)
25
+ const value = valueType == "string"
26
+ ? buffers[1].toString(encoding)
27
+ : buffers[1]
28
+ const sortValue = sortValueType == "number"
29
+ ? Number(buffers[2].toString(encoding))
30
+ : buffers[2].toString(encoding)
31
+ return [key, value, sortValue]
32
+ }
33
+ const key = buffers[0].toString(encoding)
34
+ const value = valueType == "string"
35
+ ? buffers[1].toString(encoding)
36
+ : buffers[1]
37
+ return [key, value]
38
+ }
39
+
40
+ module.exports = buffersToEntry
@@ -0,0 +1,68 @@
1
+ const Readable = require("presidium/Readable")
2
+ const TextStream = require("./TextStream")
3
+ const BinaryStream = require("./BinaryStream")
4
+ const TextEntriesStream = require("./TextEntriesStream")
5
+ const BinaryEntriesStream = require("./BinaryEntriesStream")
6
+ const AsyncIterator = require("./AsyncIterator")
7
+
8
+ /**
9
+ * @name handleServerResponse
10
+ *
11
+ * @docs
12
+ * ```specscript
13
+ * module http 'https://nodejs.org/docs/latest-v26.x/api/http.html'
14
+ * module events 'https://nodejs.org/docs/latest-v26.x/api/events.html'
15
+ *
16
+ * handleServerResponse(response http.ServerResponse, options {
17
+ * textStream: boolean,
18
+ * binaryStream: boolean,
19
+ * textEntriesStream: boolean,
20
+ * binaryEntriesStream: boolean,
21
+ * delimiter: string,
22
+ * entryLength: number,
23
+ * }) -> data object|events.EventEmitter
24
+ * ```
25
+ */
26
+ async function handleServerResponse(response, options = {}) {
27
+ const {
28
+ textStream,
29
+ binaryStream,
30
+ textEntriesStream,
31
+ binaryEntriesStream,
32
+ delimiter = "",
33
+ entryLength,
34
+ } = options
35
+
36
+ const encoding = response.headers["content-encoding"] ?? "utf8"
37
+
38
+ if (response.ok) {
39
+ if (textStream) {
40
+ const stream = new TextStream(response, delimiter, encoding)
41
+ return AsyncIterator.fromEvents(stream)
42
+ }
43
+ if (binaryStream) {
44
+ const stream = new BinaryStream(response, delimiter)
45
+ return AsyncIterator.fromEvents(stream)
46
+ }
47
+ if (textEntriesStream) {
48
+ const stream = new TextEntriesStream(response, entryLength, delimiter, encoding)
49
+ return AsyncIterator.fromEvents(stream, "entry")
50
+ }
51
+ if (binaryEntriesStream) {
52
+ const stream = new BinaryEntriesStream(response, entryLength, delimiter, encoding)
53
+ return AsyncIterator.fromEvents(stream, "entry")
54
+ }
55
+ if (response.headers["content-type"] == "application/json") {
56
+ return Readable.JSON(response)
57
+ }
58
+ if (response.headers["content-type"] == "application/octet-stream") {
59
+ return Readable.Buffer(response)
60
+ }
61
+ return Readable.Text(response)
62
+ }
63
+ const message = await Readable.Text(response)
64
+ const error = new Error(message)
65
+ throw error
66
+ }
67
+
68
+ module.exports = handleServerResponse
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "presidium-db-client-server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.0",
4
+ "description": "Presidium DB client and server library",
5
+ "author": "Richard Tong",
6
+ "license": "CFOSS",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "github:richytong/presidium-db-client-server"
10
+ },
11
+ "homepage": "https://github.com/richytong/presidium-db-client-server",
12
+ "files": [
13
+ "ArrayEventEmitter.js",
14
+ "AsyncIterator.js",
15
+ "BinaryEntriesStream.js",
16
+ "BinaryStream.js",
17
+ "DatabaseHandler.js",
18
+ "DatabaseServer.js",
19
+ "DiskHashTableClient.js",
20
+ "DiskSortedHashTableClient.js",
21
+ "TextEntriesStream.js",
22
+ "TextStream.js",
23
+ "buffersToEntry.js",
24
+ "handleServerResponse.js"
25
+ ],
26
+ "keywords": [
27
+ "presidium",
28
+ "db",
29
+ "database",
30
+ "client",
31
+ "server",
32
+ "key-value",
33
+ "node",
34
+ "nodejs",
35
+ "javascript"
36
+ ],
37
+ "scripts": {
38
+ "test": "node test.js"
39
+ },
40
+ "dependencies": {}
41
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.