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