@rpcbase/server 0.364.0 → 0.366.0-fileupload.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/boot/worker.js CHANGED
@@ -2,11 +2,13 @@
2
2
  require("./shared")
3
3
 
4
4
  require("rb-plugin-worker")
5
+ require("../src/tasks")
5
6
 
6
7
  const register_queue_listener = require("../queue/register_queue_listener")
7
8
 
8
9
  const queue = require("../queue")
9
10
 
11
+
10
12
  queue.start()
11
13
 
12
14
  let _queue_listener
@@ -17,7 +19,7 @@ register_queue_listener()
17
19
  })
18
20
 
19
21
 
20
- const handle_graceful_exit = async () => {
22
+ const handle_graceful_exit = async() => {
21
23
  const close_fns = [
22
24
  queue.instance().close,
23
25
  ]
package/express/index.js CHANGED
@@ -5,9 +5,10 @@ const body_parser = require("body-parser")
5
5
  const request_ip = require("request-ip")
6
6
  const Sentry = require("@sentry/node")
7
7
 
8
- // functionality middlewares
8
+ // functional middlewares
9
9
  const auth = require("../src/auth")
10
10
  const api = require("../src/api")
11
+ const files = require("../src/files")
11
12
  const sessions = require("../src/sessions")
12
13
 
13
14
  const dev_save_coverage = require("./dev_save_coverage")
@@ -84,6 +85,7 @@ module.exports = () => {
84
85
 
85
86
  auth(app)
86
87
  api(app)
88
+ files(app)
87
89
 
88
90
  dev_save_coverage(app)
89
91
 
package/files.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/files/finalize_file_upload"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/server",
3
- "version": "0.364.0",
3
+ "version": "0.366.0-fileupload.0",
4
4
  "license": "SSPL-1.0",
5
5
  "main": "./index.js",
6
6
  "scripts": {
@@ -9,6 +9,7 @@ const queue = require("./index")
9
9
  const dispatch_worker_queue = require("./dispatch_worker_queue")
10
10
  const dispatch_indexer_queue = require("./dispatch_indexer_queue")
11
11
 
12
+
12
13
  const log = debug("rb:queue:listener")
13
14
 
14
15
  const {RB_APP_NAME, RB_TENANT_ID} = process.env
@@ -30,10 +31,12 @@ const mongoose_delete_plugin = (schema) => {
30
31
  })
31
32
 
32
33
  // TODO: add other delete operations
34
+ // TODO: this should be deleteOne ?
33
35
  // https://mongoosejs.com/docs/queries.html
34
36
  schema.post("findOneAndDelete", function(doc) {
35
37
  log("queue:findOneAndDelete", "dispatch_doc_change NYI")
36
38
  log("del PLUGIN", doc)
39
+ const change = null // TMP
37
40
  const coll_name = change.ns.coll
38
41
 
39
42
  const model_name = Object.keys(mongoose.models).find((k) => {
@@ -53,10 +56,11 @@ const mongoose_delete_plugin = (schema) => {
53
56
 
54
57
 
55
58
  const assert_doc_id = (change) => {
56
- if (process.env.NODE_ENV !== "development") return
59
+ // if (process.env.NODE_ENV !== "development") return
57
60
 
58
61
  const doc_id = change.documentKey?._id?.toString()
59
62
 
63
+ // some changes aren't on documents
60
64
  if (!doc_id) return
61
65
 
62
66
  const sub = doc_id.substring(8, 16)
@@ -70,6 +74,11 @@ const assert_doc_id = (change) => {
70
74
  }
71
75
 
72
76
  const dispatch_change_handler = (change) => {
77
+ // skip if this is a file upload
78
+ if (change.ns?.coll?.endsWith(".files") || change.ns?.coll?.endsWith(".chunks")) {
79
+ return
80
+ }
81
+
73
82
  // verify we have correct object ids
74
83
  assert_doc_id(change)
75
84
 
@@ -79,8 +88,6 @@ const dispatch_change_handler = (change) => {
79
88
  return mongoose.models[k].collection.collectionName === coll_name
80
89
  })
81
90
 
82
- console.log("O3333K", Object.keys(require("../mongoose").models), Object.keys(mongoose.models))
83
-
84
91
  if (!model_name) {
85
92
  return
86
93
  }
@@ -107,8 +114,11 @@ const register_db_emitter = () => {
107
114
 
108
115
  // Set up the change stream with a filter to only listen to the specific database
109
116
  const pipeline = [
110
- { $match: { 'ns.db': RB_APP_NAME } },
111
- { $match: { operationType: { $in: ["insert", "update", /*"delete"*/] } } }
117
+ { $match: { "ns.db": RB_APP_NAME } },
118
+ { $match: {
119
+ "ns.coll": { $nin: ["file-uploads.files", "file-uploads.chunks"] },
120
+ "operationType": { $in: ["insert", "update" /* "delete"*/] } },
121
+ },
112
122
  ]
113
123
 
114
124
  // https://www.mongodb.com/docs/manual/reference/method/Mongo.watch/
@@ -154,8 +164,8 @@ const register_queue_listener = () => new Promise((resolve, reject) => {
154
164
  mongoose.plugin(mongoose_delete_plugin)
155
165
 
156
166
  mongoose.connection.once("open", () => {
157
- const emitter = register_db_emitter()
158
- resolve(emitter)
167
+ const db_emitter = register_db_emitter()
168
+ resolve(db_emitter)
159
169
  })
160
170
 
161
171
  mongoose.connection.on("error", (err) => {
package/rts/index.js CHANGED
@@ -37,7 +37,7 @@ const get_query_options = (ctx, options) => {
37
37
  // https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()
38
38
  const query_options = {
39
39
  ctx,
40
- is_client: true
40
+ is_client: true,
41
41
  //
42
42
  }
43
43
 
@@ -46,8 +46,7 @@ const get_query_options = (ctx, options) => {
46
46
  }
47
47
 
48
48
  const limit = typeof options.limit === "number" ? Math.min(QUERY_MAX_LIMIT, Math.abs(options.limit)) : QUERY_MAX_LIMIT
49
- // TODO: warn if limit greater than default ?
50
- query_options.limit = options.limit
49
+ query_options.limit = limit
51
50
 
52
51
  return query_options
53
52
  }
@@ -99,15 +98,15 @@ const dispatch_doc_change = (model, doc) => {
99
98
  return
100
99
  }
101
100
 
102
-
103
101
  const query_options = get_query_options(ctx, options)
102
+ // TODO: do not redo query here, use document from change event
104
103
  const query_promise = model.find(query, options.projection, query_options)
105
104
  if (query_options.sort) {
106
105
  query_promise.sort(query_options.sort)
107
106
  }
108
- if (typeof query_options.limit === "number" && query_options.limit >= 0) {
109
- query_promise.limit(query_options.limit)
110
- }
107
+
108
+ query_promise.limit(query_options.limit)
109
+
111
110
  // WARNING: DANGER
112
111
  // TODO: we should not be doing a read here, use doc from event
113
112
  const data = await query_promise
@@ -179,8 +178,8 @@ const mongoose_txn_plugin = (schema, options) => {
179
178
 
180
179
 
181
180
  // responds to a change stream "change" event
182
- const get_dispatch_change_handler = (mongoose, model_name) => async(change) => {
183
- const queries = _queries[model_name]
181
+ const get_dispatch_change_handler = (model_name) => async(change) => {
182
+ // const queries = _queries[model_name]
184
183
 
185
184
  const model = mongoose.model(model_name)
186
185
 
@@ -203,7 +202,7 @@ const get_dispatch_change_handler = (mongoose, model_name) => async(change) => {
203
202
  }
204
203
 
205
204
 
206
- const add_change_stream = (mongoose, socket_id, {model_name, query, query_key, options}) => {
205
+ const add_change_stream = (socket_id, {model_name, query, query_key, options}) => {
207
206
  if (!model_name) throw new Error("empty model name")
208
207
 
209
208
  if (!_change_streams[model_name]) {
@@ -225,7 +224,7 @@ const add_change_stream = (mongoose, socket_id, {model_name, query, query_key, o
225
224
  fullDocumentBeforeChange: "whenAvailable",
226
225
  })
227
226
 
228
- emitter.on("change", get_dispatch_change_handler(mongoose, model_name))
227
+ emitter.on("change", get_dispatch_change_handler(model_name))
229
228
 
230
229
  emitter.on("error", (err) => {
231
230
  console.log("change listener emitter got error", err)
@@ -261,7 +260,7 @@ const add_change_stream = (mongoose, socket_id, {model_name, query, query_key, o
261
260
  }
262
261
 
263
262
 
264
- const run_query = async(mongoose, socket_id, {model_name, query, query_key, options}) => {
263
+ const run_query = async(socket_id, {model_name, query, query_key, options}) => {
265
264
  const model = mongoose.model(model_name)
266
265
 
267
266
  log("run_query", {model_name, query, query_key, options})
@@ -399,7 +398,7 @@ const rts_server = async(server) => {
399
398
  console.log("socket connected", socket.id)
400
399
  //
401
400
  socket.on("register_query", (payload) => {
402
- add_change_stream(mongoose, socket.id, payload)
401
+ add_change_stream(socket.id, payload)
403
402
  })
404
403
 
405
404
  socket.on("remove_query", (payload) => {
@@ -408,7 +407,7 @@ const rts_server = async(server) => {
408
407
 
409
408
  socket.on("run_query", (payload) => {
410
409
  try {
411
- run_query(mongoose, socket.id, payload)
410
+ run_query(socket.id, payload)
412
411
  } catch (err) {
413
412
  console.log("run_query caught error", err)
414
413
  }
@@ -0,0 +1,19 @@
1
+ import Promise from "bluebird"
2
+
3
+ import queue from "../../queue"
4
+
5
+
6
+ // after we've uploaded a file, we process it with ocr, llm vectorization, png rendering, etc
7
+ export const finalize_file_upload = async(files: Array<{hash: string}>) => {
8
+
9
+ await Promise.map(files,
10
+ async(file: {hash: string}) => {
11
+ await queue.add("finalize_file_upload", {hash: file.hash, hello: "world"}, {
12
+ jobId: `finalize_file_upload-${file.hash}`,
13
+ removeOnComplete: true,
14
+ removeOnFail: true,
15
+ })
16
+ }
17
+ )
18
+
19
+ }
@@ -0,0 +1,18 @@
1
+ import assert from "assert"
2
+ import mongoose from "../../../mongoose"
3
+
4
+
5
+ const CHUNK_SIZE = 1024 * 1024
6
+
7
+
8
+ export const get_grid_fs_bucket = (bucket_name: string, chunk_size: number) => {
9
+ assert(chunk_size === CHUNK_SIZE, "chunk_size must match default CHUNK_SIZE")
10
+
11
+ const {db} = mongoose.connection
12
+ const bucket = new mongoose.mongo.GridFSBucket(db, {
13
+ bucketName: bucket_name,
14
+ chunkSizeBytes: chunk_size,
15
+ })
16
+
17
+ return bucket
18
+ }
@@ -0,0 +1,5 @@
1
+ import {upload_chunk} from "./upload_chunk"
2
+
3
+ module.exports = (app) => {
4
+ app.post("/rb-api/v1/files/upload_chunk", upload_chunk)
5
+ }
@@ -0,0 +1,5 @@
1
+
2
+ export const finalize_file_upload = async(payload) => {
3
+ console.log("TASK:FINALIZE FILE UPLOAD")
4
+ console.log("payload", payload)
5
+ }
@@ -0,0 +1,6 @@
1
+ import queue from "../../../queue"
2
+
3
+ import {finalize_file_upload} from "./finalize_file_upload"
4
+
5
+
6
+ queue.register_task("finalize_file_upload", finalize_file_upload)
@@ -0,0 +1,82 @@
1
+ import assert from "assert"
2
+ import fs from "fs"
3
+ import {formidable, File} from "formidable"
4
+
5
+ import {get_grid_fs_bucket} from "./helpers/get_grid_fs_bucket"
6
+ import { sim_test_inject } from "../helpers/sim_test_inject"
7
+
8
+
9
+ const upload_file_to_bucket = async(file: File, metadata): Promise<void> => {
10
+ const bucket = get_grid_fs_bucket("file-uploads", metadata.chunk_size)
11
+
12
+ const chunk_filename = `${metadata.hash}.${metadata.chunk_index}`
13
+
14
+ const upload_stream = bucket.openUploadStream(chunk_filename, {
15
+ metadata,
16
+ })
17
+
18
+ const read_stream = fs.createReadStream(file.filepath)
19
+
20
+ read_stream.pipe(upload_stream)
21
+
22
+ return new Promise((resolve, reject) => {
23
+ upload_stream.on("finish", () => {
24
+ // console.log("finished uploading:", upload_stream.id)
25
+ resolve()
26
+ })
27
+ upload_stream.on("error", (error) => {
28
+ reject(error)
29
+ })
30
+ })
31
+ }
32
+
33
+
34
+ export const upload_chunk = async(req, res, next) => {
35
+ const {user_id} = req.session
36
+ assert(user_id, "upload_chunk: unable to resolve user_id")
37
+
38
+ // https://github.com/node-formidable/formidable#options
39
+ const form = formidable({})
40
+
41
+ let fields, files
42
+ try {
43
+ [fields, files] = await form.parse(req)
44
+ } catch (err) {
45
+ return res.status(500).json({error: "Failed to parse uploaded file"})
46
+ }
47
+
48
+ await sim_test_inject()
49
+ // await new Promise(resolve => setTimeout(resolve, 3000))
50
+
51
+ const file_chunk = files.file_chunk[0] as File
52
+
53
+ const metadata = {
54
+ user_id,
55
+ original_filename: fields.original_filename[0],
56
+ is_compressed: fields.is_compressed[0] === "yes",
57
+ chunk_index: parseInt(fields.chunk_index[0]),
58
+ total_chunks: parseInt(fields.total_chunks[0]),
59
+ chunk_size: parseInt(fields.chunk_size[0]),
60
+ mime_type: fields.mime_type[0],
61
+ hash: fields.hash[0],
62
+ }
63
+
64
+ await upload_file_to_bucket(file_chunk, metadata)
65
+
66
+ const result: {
67
+ status: string;
68
+ finalize_token?: string;
69
+ } = {
70
+ status: "ok",
71
+ }
72
+
73
+ const is_last_chunk = metadata.chunk_index === metadata.total_chunks - 1
74
+
75
+ if (is_last_chunk) {
76
+ result.finalize_token = metadata.hash
77
+ }
78
+
79
+ res.json({
80
+ status: "ok",
81
+ })
82
+ }
@@ -0,0 +1,21 @@
1
+ const ERROR_PROBABILITY = 0.1
2
+ const THROTTLE_PROBABILITY = 0.2
3
+ const MAX_THROTTLE_MS = 2000
4
+
5
+ // randomly inject errors or delays
6
+ export const sim_test_inject = async() => {
7
+ if (!__DEV__) {
8
+ return
9
+ }
10
+
11
+ const should_error = Math.random() > 1 - ERROR_PROBABILITY
12
+ if (should_error) {
13
+ throw new Error("random sim test error")
14
+ }
15
+
16
+ const should_throttle = Math.random() > 1 - THROTTLE_PROBABILITY
17
+ if (should_throttle) {
18
+ const throttle_delay = Math.floor(Math.random() * MAX_THROTTLE_MS)
19
+ await new Promise(resolve => setTimeout(resolve, throttle_delay))
20
+ }
21
+ }
@@ -1,6 +1,8 @@
1
1
  const queue = require("../../queue")
2
2
 
3
+ require("../files/tasks")
3
4
 
4
5
  const index_item = require("./index_item")
5
6
 
7
+
6
8
  queue.register_task("rb_index_item", index_item)
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ // this tsconfig file is only used by the linter, it is ignored in .npmignore
3
+ "extends": "../bundler-server/tsconfig.json",
4
+ "include": [
5
+ "**/*.ts",
6
+ "**/*.tsx"
7
+ ]
8
+ }